1. Skema Database (SQL)
Pertama, buat database di MySQL/MariaDB Anda (misalnya dengan nama proxmox_inventory), lalu jalankan query SQL berikut untuk membuat tabel yang diperlukan.
-- Skema untuk Aplikasi Inventaris Proxmox
-- Database: proxmox_inventory
-- Tabel untuk pengguna aplikasi
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`role` enum('admin','readonly') NOT NULL DEFAULT 'readonly',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Memasukkan user default: admin/admin dan user/user
-- PENTING: Ganti password ini di lingkungan produksi!
INSERT INTO `users` (`id`, `username`, `password`, `role`) VALUES
(1, 'admin', '$2y$10$E.qf5CgG5V5iC3j2b.hRTeQh8.vK1a.wT5.g.sU4.zL5U6.iC7.yO', 'admin'), -- password: admin
(2, 'user', '$2y$10$w9yL9k7J8c.gR4.bN3.fIeT6.hJ5.vK0.aW2.xY3.zL4.uO1.zX9q', 'readonly'); -- password: user
-- Tabel untuk menyimpan data server Proxmox
CREATE TABLE `proxmox_servers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`ip_address` varchar(255) NOT NULL,
`node_name` varchar(100) NOT NULL,
`api_user` varchar(100) NOT NULL,
`api_token_name` varchar(100) NOT NULL,
`api_token_secret` varchar(255) NOT NULL, -- Di produksi, kolom ini sebaiknya dienkripsi
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
2. Kode Aplikasi (PHP, HTML, JS)
Berikut adalah semua file kode yang dibutuhkan. Buat struktur direktori seperti yang disarankan, lalu salin setiap blok kode ke dalam file yang sesuai.
<?php // File: config.php
/**
* File Konfigurasi Aplikasi
* -------------------------
* Sesuaikan pengaturan database dan path aplikasi di sini.
*/
// Konfigurasi Database
define('DB_HOST', 'localhost');
define('DB_NAME', 'proxmox_inventory');
define('DB_USER', 'root'); // Ganti dengan user database Anda
define('DB_PASS', ''); // Ganti dengan password database Anda
// URL Aplikasi
// Pastikan ini adalah path root dari aplikasi Anda di web server
define('BASE_URL', '/proxmox-inventory/public');
// Path Aplikasi di Server
define('ROOT_PATH', dirname(__DIR__));
define('APP_PATH', ROOT_PATH . '/app');
?>
```php
<?php // File: public/.htaccess
/**
* Konfigurasi Apache untuk Clean URLs
* ------------------------------------
* File ini mengarahkan semua request yang bukan file atau direktori
* yang ada ke index.php (Front Controller).
*/
RewriteEngine On
# Arahkan semua request ke front controller
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
```php
<?php // File: public/index.php
/**
* Front Controller
* ----------------
* Titik masuk utama untuk semua request.
* Memuat konfigurasi, memulai session, dan memanggil router.
*/
// --- UNTUK DEBUGGING ---
// Tampilkan semua error di browser. HAPUS ATAU KOMENTARI DI LINGKUNGAN PRODUKSI!
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// -------------------------
// Mulai session
session_start();
// Muat file konfigurasi
require_once '../config.php';
// Autoloader sederhana untuk class
spl_autoload_register(function ($className) {
$file = ROOT_PATH . '/' . str_replace('\\', '/', $className) . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// Muat Router dan jalankan
require_once APP_PATH . '/Core/Router.php';
$router = new App\Core\Router();
// --- Definisi Rute (Routes) ---
// Rute Publik (Login)
$router->add('GET', '/login', 'App\Controllers\AuthController@showLogin');
$router->add('POST', '/login', 'App\Controllers\AuthController@login');
$router->add('GET', '/logout', 'App\Controllers\AuthController@logout');
// Rute yang Dilindungi (Membutuhkan Login)
$router->add('GET', '/dashboard', 'App\Controllers\DashboardController@index', ['auth']);
$router->add('GET', '/servers', 'App\Controllers\ServerController@index', ['auth']);
$router->add('GET', '/servers/create', 'App\Controllers\ServerController@create', ['auth', 'admin']);
$router->add('POST', '/servers', 'App\Controllers\ServerController@store', ['auth', 'admin']);
$router->add('GET', '/servers/(\d+)', 'App\Controllers\ServerController@show', ['auth']);
$router->add('GET', '/servers/(\d+)/edit', 'App\Controllers\ServerController@edit', ['auth', 'admin']);
$router->add('POST', '/servers/(\d+)', 'App\Controllers\ServerController@update', ['auth', 'admin']);
$router->add('POST', '/servers/(\d+)/delete', 'App\Controllers\ServerController@destroy', ['auth', 'admin']);
// Rute API untuk AJAX
$router->add('GET', '/api/check-status/server/(\d+)', 'App\Controllers\ApiController@checkServerStatus', ['auth']);
$router->add('GET', '/api/check-status/vm/(\d+)/(.+)', 'App\Controllers\ApiController@checkVmStatus', ['auth']);
// Jalankan router
$router->dispatch();
```php
<?php // File: app/Core/Router.php
namespace App\Core;
class Router {
protected $routes = [];
protected $middlewares = [
'auth' => \App\Middleware\AuthMiddleware::class,
'admin' => \App\Middleware\AdminMiddleware::class,
];
public function add($method, $uri, $controller, $middlewares = []) {
$this->routes[] = [
'method' => $method,
'uri' => $uri,
'controller' => $controller,
'middlewares' => $middlewares,
];
}
public function dispatch() {
$requestUri = '/' . trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
$base_path = trim(BASE_URL, '/');
if (strpos($requestUri, $base_path) === 0) {
$requestUri = substr($requestUri, strlen($base_path));
}
$requestUri = $requestUri ?: '/';
$requestMethod = $_SERVER['REQUEST_METHOD'];
foreach ($this->routes as $route) {
$pattern = "#^" . $route['uri'] . "$#";
if ($route['method'] === $requestMethod && preg_match($pattern, $requestUri, $matches)) {
array_shift($matches);
// Jalankan Middleware
foreach ($route['middlewares'] as $middlewareKey) {
if (isset($this->middlewares[$middlewareKey])) {
(new $this->middlewares[$middlewareKey])->handle();
}
}
list($controller, $method) = explode('@', $route['controller']);
$controllerInstance = new $controller();
call_user_func_array([$controllerInstance, $method], $matches);
return;
}
}
// Jika tidak ada rute yang cocok
http_response_code(404);
$this->view('errors/404');
}
protected function view($view, $data = []) {
extract($data);
$viewPath = APP_PATH . "/Views/{$view}.php";
if (file_exists($viewPath)) {
require_once $viewPath;
} else {
echo "404 Not Found";
}
}
}
```php
<?php // File: app/Core/Database.php
namespace App\Core;
use PDO;
use PDOException;
class Database {
private static $instance = null;
private $conn;
private function __construct() {
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8';
try {
$this->conn = new PDO($dsn, DB_USER, DB_PASS);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
} catch (PDOException $e) {
die('Connection Failed: ' . $e->getMessage());
}
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new Database();
}
return self::$instance->conn;
}
}
```php
<?php // File: app/Core/Controller.php
namespace App\Core;
class Controller {
protected function view($view, $data = []) {
extract($data);
$viewPath = APP_PATH . "/Views/{$view}.php";
if (file_exists($viewPath)) {
require_once $viewPath;
} else {
die("View {$view} not found.");
}
}
}
```php
<?php // File: app/Core/ProxmoxAPI.php
namespace App\Core;
class ProxmoxAPI {
private $hostname;
private $apiUser;
private $apiTokenName;
private $apiTokenSecret;
public function __construct($hostname, $apiUser, $apiTokenName, $apiTokenSecret) {
$this->hostname = rtrim($hostname, '/');
$this->apiUser = $apiUser;
$this->apiTokenName = $apiTokenName;
$this->apiTokenSecret = $apiTokenSecret;
}
public function get($path) {
$url = "https://{$this->hostname}:8006/api2/json{$path}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: PVEAPIToken={$this->apiUser}!{$this->apiTokenName}={$this->apiTokenSecret}"
]);
// Menonaktifkan verifikasi SSL (HATI-HATI: hanya untuk development jika sertifikat self-signed)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new \Exception("cURL Error: " . $error);
}
curl_close($ch);
if ($http_code >= 400) {
throw new \Exception("Proxmox API Error (HTTP {$http_code}): {$response}");
}
$data = json_decode($response, true);
return $data['data'] ?? null;
}
public function getVMs($node) {
return $this->get("/nodes/{$node}/qemu");
}
public function getVmIpAddress($node, $vmid) {
try {
$interfaces = $this->get("/nodes/{$node}/qemu/{$vmid}/agent/get-network-ifaces");
if (isset($interfaces['result']) && is_array($interfaces['result'])) {
foreach ($interfaces['result'] as $iface) {
if (isset($iface['ip-addresses'])) {
foreach ($iface['ip-addresses'] as $ip) {
// Ambil IPv4 pertama yang bukan loopback
if ($ip['ip-address-type'] === 'ipv4' && $ip['ip-address'] !== '127.0.0.1') {
return $ip['ip-address'];
}
}
}
}
}
} catch (\Exception $e) {
// Abaikan jika agent tidak aktif atau tidak ada IP
return 'N/A';
}
return 'N/A';
}
}
```php
<?php // File: app/Middleware/AuthMiddleware.php
namespace App\Middleware;
class AuthMiddleware {
public function handle() {
if (!isset($_SESSION['user_id'])) {
header('Location: ' . BASE_URL . '/login');
exit();
}
}
}
```php
<?php // File: app/Middleware/AdminMiddleware.php
namespace App\Middleware;
class AdminMiddleware {
public function handle() {
if (!isset($_SESSION['user_role']) || $_SESSION['user_role'] !== 'admin') {
// Bisa redirect ke halaman 'unauthorized' atau dashboard
$_SESSION['error_message'] = "You do not have permission to access this page.";
header('Location: ' . BASE_URL . '/dashboard');
exit();
}
}
}
```php
<?php // File: app/Models/User.php
namespace App\Models;
use App\Core\Database;
use PDO;
class User {
private $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function findByUsername($username) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
return $stmt->fetch();
}
}
```php
<?php // File: app/Models/Server.php
namespace App\Models;
use App\Core\Database;
use PDO;
class Server {
private $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getAll() {
$stmt = $this->db->query("SELECT * FROM proxmox_servers ORDER BY name ASC");
return $stmt->fetchAll();
}
public function findById($id) {
$stmt = $this->db->prepare("SELECT * FROM proxmox_servers WHERE id = :id");
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}
public function create($data) {
$stmt = $this->db->prepare(
"INSERT INTO proxmox_servers (name, ip_address, node_name, api_user, api_token_name, api_token_secret)
VALUES (:name, :ip_address, :node_name, :api_user, :api_token_name, :api_token_secret)"
);
return $stmt->execute($data);
}
public function update($id, $data) {
$data['id'] = $id;
$stmt = $this->db->prepare(
"UPDATE proxmox_servers SET name = :name, ip_address = :ip_address, node_name = :node_name,
api_user = :api_user, api_token_name = :api_token_name, api_token_secret = :api_token_secret
WHERE id = :id"
);
return $stmt->execute($data);
}
public function delete($id) {
$stmt = $this->db->prepare("DELETE FROM proxmox_servers WHERE id = :id");
return $stmt->execute(['id' => $id]);
}
}
```php
<?php // File: app/Controllers/AuthController.php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
class AuthController extends Controller {
public function showLogin() {
if (isset($_SESSION['user_id'])) {
header('Location: ' . BASE_URL . '/dashboard');
exit();
}
$this->view('auth/login');
}
public function login() {
$userModel = new User();
$user = $userModel->findByUsername($_POST['username']);
if ($user && password_verify($_POST['password'], $user->password)) {
$_SESSION['user_id'] = $user->id;
$_SESSION['username'] = $user->username;
$_SESSION['user_role'] = $user->role;
header('Location: ' . BASE_URL . '/dashboard');
exit();
} else {
$_SESSION['error_message'] = 'Invalid username or password.';
header('Location: ' . BASE_URL . '/login');
exit();
}
}
public function logout() {
session_destroy();
header('Location: ' . BASE_URL . '/login');
exit();
}
}
```php
<?php // File: app/Controllers/DashboardController.php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Server;
class DashboardController extends Controller {
public function index() {
$serverModel = new Server();
$servers = $serverModel->getAll();
$this->view('dashboard/index', ['servers' => $servers]);
}
}
```php
<?php // File: app/Controllers/ServerController.php
namespace App\Controllers;
use App\Core\Controller;
use App\Core\ProxmoxAPI;
use App\Models\Server;
class ServerController extends Controller {
private $serverModel;
public function __construct() {
$this->serverModel = new Server();
}
public function index() {
$servers = $this->serverModel->getAll();
$this->view('servers/index', ['servers' => $servers]);
}
public function create() {
$this->view('servers/create');
}
public function store() {
$data = [
'name' => $_POST['name'],
'ip_address' => $_POST['ip_address'],
'node_name' => $_POST['node_name'],
'api_user' => $_POST['api_user'],
'api_token_name' => $_POST['api_token_name'],
'api_token_secret' => $_POST['api_token_secret'],
];
if ($this->serverModel->create($data)) {
$_SESSION['success_message'] = 'Server added successfully.';
} else {
$_SESSION['error_message'] = 'Failed to add server.';
}
header('Location: ' . BASE_URL . '/servers');
exit();
}
public function show($id) {
$server = $this->serverModel->findById($id);
if (!$server) {
http_response_code(404);
echo "Server not found";
return;
}
$vms = [];
$error = null;
try {
$api = new ProxmoxAPI($server->ip_address, $server->api_user, $server->api_token_name, $server->api_token_secret);
$vmsData = $api->getVMs($server->node_name);
if ($vmsData) {
foreach ($vmsData as $vmData) {
$vmData['ip_address'] = $api->getVmIpAddress($server->node_name, $vmData['vmid']);
$vms[] = $vmData;
}
}
} catch (\Exception $e) {
$error = "Failed to connect to Proxmox API: " . $e->getMessage();
}
$this->view('servers/show', ['server' => $server, 'vms' => $vms, 'error' => $error]);
}
public function edit($id) {
$server = $this->serverModel->findById($id);
$this->view('servers/edit', ['server' => $server]);
}
public function update($id) {
$server = $this->serverModel->findById($id);
// Gunakan secret yang ada jika input password kosong
$secret = !empty($_POST['api_token_secret']) ? $_POST['api_token_secret'] : $server->api_token_secret;
$data = [
'name' => $_POST['name'],
'ip_address' => $_POST['ip_address'],
'node_name' => $_POST['node_name'],
'api_user' => $_POST['api_user'],
'api_token_name' => $_POST['api_token_name'],
'api_token_secret' => $secret,
];
if ($this->serverModel->update($id, $data)) {
$_SESSION['success_message'] = 'Server updated successfully.';
} else {
$_SESSION['error_message'] = 'Failed to update server.';
}
header('Location: ' . BASE_URL . '/servers');
exit();
}
public function destroy($id) {
if ($this->serverModel->delete($id)) {
$_SESSION['success_message'] = 'Server deleted successfully.';
} else {
$_SESSION['error_message'] = 'Failed to delete server.';
}
header('Location: ' . BASE_URL . '/servers');
exit();
}
}
```php
<?php // File: app/Controllers/ApiController.php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Server;
class ApiController extends Controller {
private function checkPing($host, $port = 8006, $timeout = 1) {
// Gunakan @ untuk menekan warning jika host tidak ditemukan
$fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
if ($fp) {
fclose($fp);
return true;
}
return false;
}
public function checkServerStatus($id) {
$serverModel = new Server();
$server = $serverModel->findById($id);
header('Content-Type: application/json');
if ($server) {
$status = $this->checkPing($server->ip_address);
echo json_encode(['status' => $status ? 'UP' : 'DOWN']);
} else {
http_response_code(404);
echo json_encode(['status' => 'NOT_FOUND']);
}
}
public function checkVmStatus($serverId, $vmIp) {
// Cek server dulu untuk keamanan, meski tidak digunakan langsung
$serverModel = new Server();
$server = $serverModel->findById($serverId);
header('Content-Type: application/json');
if ($server && filter_var($vmIp, FILTER_VALIDATE_IP)) {
// Cek ping ke port umum seperti 22 (SSH) atau 3389 (RDP) dengan timeout singkat
$status = $this->checkPing($vmIp, 22, 0.5) || $this->checkPing($vmIp, 3389, 0.5);
echo json_encode(['status' => $status ? 'UP' : 'DOWN']);
} else {
http_response_code(400);
echo json_encode(['status' => 'INVALID_INPUT']);
}
}
}
```php
<?php // File: app/Views/layouts/header.php ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?? 'Proxmox Inventory' ?></title>
<link href="[https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css](https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css)" rel="stylesheet">
<link rel="stylesheet" href="[https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css](https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css)">
<style>
body { background-color: #f8f9fa; }
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,.1); }
.card { border: none; box-shadow: 0 0 15px rgba(0,0,0,.05); }
.status-dot {
height: 12px;
width: 12px;
border-radius: 50%;
display: inline-block;
}
.status-up {
background-color: #198754;
animation: pulse-green 2s infinite;
}
.status-down { background-color: #dc3545; }
.status-unknown { background-color: #6c757d; }
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(25, 135, 84, 0); }
100% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0); }
}
</style>
</head>
<body>
<?php if (isset($_SESSION['user_id'])): ?>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand" href="<?= BASE_URL ?>/dashboard">
<i class="bi bi-hdd-stack"></i> Proxmox Inventory
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="<?= BASE_URL ?>/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= BASE_URL ?>/servers">Manage Servers</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> <?= htmlspecialchars($_SESSION['username']) ?> (<?= htmlspecialchars($_SESSION['user_role']) ?>)
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="<?= BASE_URL ?>/logout">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<?php endif; ?>
<main class="container">
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $_SESSION['success_message'] ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= $_SESSION['error_message'] ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
```php
<?php // File: app/Views/layouts/footer.php ?>
</main>
<footer class="text-center text-muted py-4 mt-5">
<p>© <?= date('Y') ?> Proxmox Inventory. Dibuat dengan PHP Native.</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
```php
<?php // File: app/Views/auth/login.php ?>
<?php $this->view('layouts/header', ['title' => 'Login']); ?>
<div class="d-flex align-items-center justify-content-center" style="min-height: 80vh;">
<div class="card" style="width: 100%; max-width: 400px;">
<div class="card-body p-4">
<h3 class="card-title text-center mb-4">Proxmox Inventory Login</h3>
<form action="<?= BASE_URL ?>/login" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
<div class="text-center mt-3">
<small class="text-muted">Default: admin/admin or user/user</small>
</div>
</div>
</div>
</div>
<?php $this->view('layouts/footer'); ?>
```php
<?php // File: app/Views/dashboard/index.php ?>
<?php $this->view('layouts/header', ['title' => 'Dashboard']); ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Dashboard</h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
Server Overview
</div>
<div class="card-body">
<?php if (empty($servers)): ?>
<p class="text-center">No servers added yet.
<?php if ($_SESSION['user_role'] === 'admin'): ?>
<a href="<?= BASE_URL ?>/servers/create">Add a new server</a> to get started.
<?php endif; ?>
</p>
<?php else: ?>
<div class="list-group">
<?php foreach ($servers as $server): ?>
<a href="<?= BASE_URL ?>/servers/<?= $server->id ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1"><?= htmlspecialchars($server->name) ?></h5>
<small><?= htmlspecialchars($server->ip_address) ?></small>
</div>
<span class="badge bg-light text-dark p-2">
Status:
<span class="status-dot status-unknown" id="status-server-<?= $server->id ?>" title="Checking status..."></span>
</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const servers = document.querySelectorAll('[id^="status-server-"]');
servers.forEach(el => {
const serverId = el.id.split('-')[2];
checkServerStatus(serverId);
});
function checkServerStatus(serverId) {
const statusEl = document.getElementById(`status-server-${serverId}`);
fetch(`<?= BASE_URL ?>/api/check-status/server/${serverId}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
statusEl.classList.remove('status-unknown', 'status-up', 'status-down');
if (data.status === 'UP') {
statusEl.classList.add('status-up');
statusEl.title = 'Server is UP';
} else {
statusEl.classList.add('status-down');
statusEl.title = 'Server is DOWN';
}
})
.catch(error => {
console.error('Error checking status:', error);
statusEl.classList.remove('status-unknown', 'status-up');
statusEl.classList.add('status-down');
statusEl.title = 'Error checking status';
});
}
});
</script>
<?php $this->view('layouts/footer'); ?>
```php
<?php // File: app/Views/servers/index.php ?>
<?php $this->view('layouts/header', ['title' => 'Manage Servers']); ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Manage Servers</h1>
<?php if ($_SESSION['user_role'] === 'admin'): ?>
<a href="<?= BASE_URL ?>/servers/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Server
</a>
<?php endif; ?>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Name</th>
<th>IP Address</th>
<th>Node</th>
<th>API User</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($servers)): ?>
<tr>
<td colspan="6" class="text-center">No servers found.</td>
</tr>
<?php else: ?>
<?php foreach ($servers as $server): ?>
<tr>
<td><?= htmlspecialchars($server->name) ?></td>
<td><?= htmlspecialchars($server->ip_address) ?></td>
<td><?= htmlspecialchars($server->node_name) ?></td>
<td><?= htmlspecialchars($server->api_user) ?></td>
<td>
<span class="status-dot status-unknown" id="status-server-<?= $server->id ?>" title="Checking status..."></span>
</td>
<td>
<a href="<?= BASE_URL ?>/servers/<?= $server->id ?>" class="btn btn-sm btn-info" title="View VMs"><i class="bi bi-eye"></i></a>
<?php if ($_SESSION['user_role'] === 'admin'): ?>
<a href="<?= BASE_URL ?>/servers/<?= $server->id ?>/edit" class="btn btn-sm btn-warning" title="Edit"><i class="bi bi-pencil"></i></a>
<form action="<?= BASE_URL ?>/servers/<?= $server->id ?>/delete" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this server?');">
<button type="submit" class="btn btn-sm btn-danger" title="Delete"><i class="bi bi-trash"></i></button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Skrip ini sama dengan yang ada di dashboard untuk mengecek status server
document.addEventListener('DOMContentLoaded', function() {
const servers = document.querySelectorAll('[id^="status-server-"]');
servers.forEach(el => {
const serverId = el.id.split('-')[2];
fetch(`<?= BASE_URL ?>/api/check-status/server/${serverId}`)
.then(response => response.json())
.then(data => {
const statusEl = document.getElementById(`status-server-${serverId}`);
statusEl.classList.remove('status-unknown');
if (data.status === 'UP') {
statusEl.classList.add('status-up');
statusEl.title = 'UP';
} else {
statusEl.classList.add('status-down');
statusEl.title = 'DOWN';
}
});
});
});
</script>
<?php $this->view('layouts/footer'); ?>
```php
<?php // File: app/Views/servers/create.php ?>
<?php $this->view('layouts/header', ['title' => 'Add New Server']); ?>
<h1>Add New Server</h1>
<p class="text-muted">Add a new Proxmox server to the inventory.</p>
<div class="card">
<div class="card-body">
<form action="<?= BASE_URL ?>/servers" method="POST">
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Server Name</label>
<input type="text" class="form-control" id="name" name="name" required placeholder="e.g., PVE-DC1">
</div>
<div class="col-md-6 mb-3">
<label for="ip_address" class="form-label">IP Address / Hostname</label>
<input type="text" class="form-control" id="ip_address" name="ip_address" required placeholder="e.g., 192.168.1.10">
</div>
</div>
<div class="mb-3">
<label for="node_name" class="form-label">Node Name</label>
<input type="text" class="form-control" id="node_name" name="node_name" required placeholder="The name of the node in Proxmox cluster">
</div>
<hr>
<h5 class="mb-3">API Credentials</h5>
<div class="row">
<div class="col-md-4 mb-3">
<label for="api_user" class="form-label">API User</label>
<input type="text" class="form-control" id="api_user" name="api_user" required placeholder="e.g., root@pam!mytoken">
</div>
<div class="col-md-4 mb-3">
<label for="api_token_name" class="form-label">API Token Name</label>
<input type="text" class="form-control" id="api_token_name" name="api_token_name" required placeholder="e.g., inventory-app">
</div>
<div class="col-md-4 mb-3">
<label for="api_token_secret" class="form-label">API Token Secret</label>
<input type="password" class="form-control" id="api_token_secret" name="api_token_secret" required>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Save Server</button>
<a href="<?= BASE_URL ?>/servers" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<?php $this->view('layouts/footer'); ?>
```php
<?php // File: app/Views/servers/edit.php ?>
<?php $this->view('layouts/header', ['title' => 'Edit Server']); ?>
<h1>Edit Server: <?= htmlspecialchars($server->name) ?></h1>
<div class="card">
<div class="card-body">
<form action="<?= BASE_URL ?>/servers/<?= $server->id ?>" method="POST">
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Server Name</label>
<input type="text" class="form-control" id="name" name="name" required value="<?= htmlspecialchars($server->name) ?>">
</div>
<div class="col-md-6 mb-3">
<label for="ip_address" class="form-label">IP Address / Hostname</label>
<input type="text" class="form-control" id="ip_address" name="ip_address" required value="<?= htmlspecialchars($server->ip_address) ?>">
</div>
</div>
<div class="mb-3">
<label for="node_name" class="form-label">Node Name</label>
<input type="text" class="form-control" id="node_name" name="node_name" required value="<?= htmlspecialchars($server->node_name) ?>">
</div>
<hr>
<h5 class="mb-3">API Credentials</h5>
<div class="row">
<div class="col-md-4 mb-3">
<label for="api_user" class="form-label">API User</label>
<input type="text" class="form-control" id="api_user" name="api_user" required value="<?= htmlspecialchars($server->api_user) ?>">
</div>
<div class="col-md-4 mb-3">
<label for="api_token_name" class="form-label">API Token Name</label>
<input type="text" class="form-control" id="api_token_name" name="api_token_name" required value="<?= htmlspecialchars($server->api_token_name) ?>">
</div>
<div class="col-md-4 mb-3">
<label for="api_token_secret" class="form-label">API Token Secret</label>
<input type="password" class="form-control" id="api_token_secret" name="api_token_secret" placeholder="Leave blank to keep current secret">
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Update Server</button>
<a href="<?= BASE_URL ?>/servers" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<?php $this->view('layouts/footer'); ?>
```php
<?php // File: app/Views/servers/show.php ?>
<?php $this->view('layouts/header', ['title' => 'Server Details']); ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-server"></i> <?= htmlspecialchars($server->name) ?></h1>
<a href="<?= BASE_URL ?>/servers" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Servers
</a>
</div>
<?php if ($error): ?>
<div class="alert alert-danger">
<strong>API Error:</strong> <?= htmlspecialchars($error) ?>
</div>
<?php else: ?>
<div class="card">
<div class="card-header">
Virtual Machines
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Status</th>
<th>VMID</th>
<th>Name</th>
<th>IP Address</th>
<th>OS</th>
<th>CPU</th>
<th>RAM</th>
<th>Disk</th>
<th>Ping</th>
</tr>
</thead>
<tbody>
<?php if (empty($vms)): ?>
<tr>
<td colspan="9" class="text-center">No Virtual Machines found on this server.</td>
</tr>
<?php else: ?>
<?php foreach ($vms as $vm): ?>
<tr>
<td>
<?php if ($vm['status'] === 'running'): ?>
<span class="badge bg-success">Running</span>
<?php else: ?>
<span class="badge bg-secondary">Stopped</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($vm['vmid']) ?></td>
<td><?= htmlspecialchars($vm['name']) ?></td>
<td><?= htmlspecialchars($vm['ip_address']) ?></td>
<td><i class="bi bi-ubuntu"></i> Linux</td> <!-- Placeholder -->
<td><?= htmlspecialchars($vm['cpus']) ?> Core(s)</td>
<td><?= round($vm['maxmem'] / 1024 / 1024 / 1024, 2) ?> GB</td>
<td><?= round($vm['maxdisk'] / 1024 / 1024 / 1024, 2) ?> GB</td>
<td>
<?php if ($vm['status'] === 'running' && $vm['ip_address'] !== 'N/A'): ?>
<span class="status-dot status-unknown" id="status-vm-<?= $vm['vmid'] ?>" data-ip="<?= htmlspecialchars($vm['ip_address']) ?>" title="Checking status..."></span>
<?php else: ?>
<span class="status-dot status-down" title="VM is stopped or IP is unavailable"></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const vms = document.querySelectorAll('[id^="status-vm-"]');
vms.forEach(el => {
const vmId = el.id.split('-')[2];
const vmIp = el.dataset.ip;
if (vmIp && vmIp !== 'N/A') {
checkVmStatus(vmId, vmIp);
}
});
function checkVmStatus(vmId, vmIp) {
const statusEl = document.getElementById(`status-vm-${vmId}`);
const serverId = <?= $server->id ?>;
fetch(`<?= BASE_URL ?>/api/check-status/vm/${serverId}/${vmIp}`)
.then(response => response.json())
.then(data => {
statusEl.classList.remove('status-unknown', 'status-up', 'status-down');
if (data.status === 'UP') {
statusEl.classList.add('status-up');
statusEl.title = 'VM is UP';
} else {
statusEl.classList.add('status-down');
statusEl.title = 'VM is DOWN';
}
})
.catch(error => {
console.error(`Error checking VM ${vmId} status:`, error);
statusEl.classList.remove('status-unknown', 'status-up');
statusEl.classList.add('status-down');
statusEl.title = 'Error checking status';
});
}
});
</script>
<?php $this->view('layouts/footer'); ?>
```php
<?php // File: app/Views/errors/404.php ?>
<?php $this->view('layouts/header', ['title' => 'Not Found']); ?>
<div class="text-center py-5">
<h1 class="display-1">404</h1>
<h2>Page Not Found</h2>
<p class="lead">
Sorry, the page you are looking for does not exist.
</p>
<a href="<?= BASE_URL ?>/dashboard" class="btn btn-primary">Go to Dashboard</a>
</div>
<?php $this->view('layouts/footer'); ?>
Tentu, saya akan buatkan aplikasi inventaris Proxmox VE lengkap sesuai dengan permintaan Anda. Aplikasi ini akan menggunakan PHP native dengan struktur yang rapi, sistem routing, dan dua level hak akses pengguna.
Saya akan menyajikan semua kode yang Anda butuhkan dalam satu dokumen. Anda hanya perlu menyalin file-file ini ke dalam struktur direktori yang benar di server web Anda.
1. Skema Database (SQL)
Pertama, buat database di MySQL/MariaDB Anda (misalnya dengan nama proxmox_inventory), lalu jalankan query SQL berikut untuk membuat tabel yang diperlukan.
Skema Database (SQL)
13 Agu, 19.41Buka
2. Kode Aplikasi (PHP, HTML, JS)
Berikut adalah semua file kode yang dibutuhkan. Buat struktur direktori seperti yang disarankan, lalu salin setiap blok kode ke dalam file yang sesuai.
Aplikasi Inventaris Proxmox (PHP & HTML)
13 Agu, 20.15
3. Cara Menjalankan Aplikasi
- Siapkan Lingkungan: Pastikan Anda memiliki web server (Apache/Nginx), PHP 8.x, dan MySQL/MariaDB terinstal.
- Struktur Direktori: Buat struktur direktori seperti ini:
/ (root direktori web Anda, misal: /var/www/html)
└── /proxmox-inventory
├── /app
│ ├── /Controllers
│ ├── /Core
│ ├── /Middleware
│ ├── /Models
│ └── /Views
│ ├── /auth
│ ├── /dashboard
│ ├── /layouts
│ └── /servers
├── /public
│ ├── .htaccess
│ └── index.php
└── config.php
- Salin File: Salin setiap blok kode di atas ke dalam file dan lokasi yang sesuai.
- Konfigurasi
config.php: Bukaconfig.phpdan sesuaikan detail koneksi database (DB_HOST,DB_NAME,DB_USER,DB_PASS) danBASE_URLjika aplikasi Anda tidak berada di/proxmox-inventory/public. - Database: Import file SQL yang disediakan ke dalam database Anda. Ini akan membuat tabel dan user default (
admin/admindanuser/user). - Konfigurasi Apache: Pastikan modul
mod_rewritedi Apache sudah aktif. File.htaccessakan menangani clean URLs. - Akses Aplikasi: Buka browser Anda dan arahkan ke
http://localhost/proxmox-inventory/public. Anda akan disambut oleh halaman login.
Aplikasi ini sekarang siap digunakan. Anda bisa login sebagai admin untuk mulai menambahkan server Proxmox Anda dan melihat daftar VM secara otomatis.