0) { ob_end_flush(); } ob_implicit_flush(true); // Send headers early header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-cache'); header('X-Accel-Buffering: no'); // Hint for Nginx: disable buffering // Basic config $woPath = '/usr/local/bin/wo'; $bootstrapScript = '/usr/local/bin/wp-dev-bootstrap.sh'; // Defaults for bootstrap script $bootstrapAdminUser = 'vdidev'; $bootstrapAdminEmail = 'dev@vincentdevelopment.ca'; // Optional theme config for bootstrap script $themeStarterRepo = 'git@github.com:Vincent-Design-Inc/VDI-Starter-v5.git'; $themeRemoteOrigin = ''; // e.g. 'git@github.com:your-org/client-theme-repo.git' // SQLite config $dbPath = __DIR__ . '/panel.sqlite'; // ---------- Init DB ---------- $seedInfo = initDb(); // ---------- Routing / Auth Gate ---------- $action = isset($_GET['action']) ? $_GET['action'] : 'list'; // Logout handler if ($action === 'logout') { $_SESSION = []; session_destroy(); header('Location: ?action=login'); exit; } requireLogin($action); $user = getCurrentUser(); // ---------- Login Action ---------- if ($action === 'login') { $loginError = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; if ($username !== '' && $password !== '') { $db = getDb(); $stmt = $db->prepare('SELECT id, username, password_hash, role FROM users WHERE username = ?'); $stmt->execute([$username]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row && password_verify($password, $row['password_hash'])) { $_SESSION['user_id'] = $row['id']; $_SESSION['username'] = $row['username']; $_SESSION['role'] = $row['role']; header('Location: ?action=list'); exit; } else { $loginError = 'Invalid username or password.'; } } else { $loginError = 'Username and password are required.'; } } ?> WordOps Dev Panel - Login

WordOps Dev Panel

Sign in to manage dev sites.

Login

Initial admin created.
Username: admin
Password: change-me. Please log in and change it later.

'; } if ($loginError) { echo '

' . htmlspecialchars($loginError) . '

'; } ?>
prepare('SELECT password_hash FROM users WHERE id = ?'); $stmt->execute([$user['id']]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row || !password_verify($currentPassword, $row['password_hash'])) { $selfPasswordError = 'Current password is incorrect.'; } else { $hash = password_hash($newPassword, PASSWORD_DEFAULT); $update = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); $update->execute([$hash, $user['id']]); $selfPasswordMessage = 'Your password has been updated.'; } } } // ---------- Page Layout (for authenticated users) ---------- ?> WordOps Dev Panel

WordOps Dev Panel

Create and manage dev sites.

Invalid domain. Use only letters, numbers, dots and dashes.'; } else { // Build WordOps command $flags = []; if ($stackType === 'wp') { $flags[] = '--wp'; } elseif ($stackType === 'wpfc') { $flags[] = '--wpfc'; } elseif ($stackType === 'plain') { $flags[] = '--php'; } if ($isMultisite === 'subdir') { $flags[] = '--wpsubdir'; } elseif ($isMultisite === 'subdomain') { $flags[] = '--wpsubdomain'; } $cmd = sprintf( 'sudo %s site create %s %s', escapeshellcmd($woPath), escapeshellarg($domain), implode(' ', array_map('escapeshellarg', $flags)) ); echo '
'; echo '

Provisioning site: ' . htmlspecialchars($domain) . '

'; echo '

WordOps log:

'; echo '
';

      @ob_flush();
      @flush();

      list($ok, $woLines) = runCommandStreaming($cmd, function ($line) {
        echo htmlspecialchars($line) . "\n";
        @ob_flush();
        @flush();
      });

      echo '
'; if ($ok) { // Record site owner in SQLite $db = getDb(); $stmt = $db->prepare('INSERT OR REPLACE INTO sites (domain, owner_id, created_at) VALUES (?, ?, datetime("now"))'); $stmt->execute([$domain, $user['id']]); echo '

Site created successfully.

'; // Run bootstrap if requested if ($bootstrapProfile === 'standard') { $adminUser = $bootstrapAdminUser; $adminEmail = $bootstrapAdminEmail; $bootstrapArgs = [ '--domain', $domain, '--project-name', $projectName, '--admin-user', $adminUser, '--admin-email', $adminEmail, ]; if ($themeStarterRepo !== '') { $bootstrapArgs[] = '--theme-starter-repo'; $bootstrapArgs[] = $themeStarterRepo; } if ($themeRemoteOrigin !== '') { $bootstrapArgs[] = '--theme-remote-origin'; $bootstrapArgs[] = $themeRemoteOrigin; } $bootstrapCmd = escapeshellcmd($bootstrapScript); foreach ($bootstrapArgs as $arg) { $bootstrapCmd .= ' ' . escapeshellarg($arg); } echo '

Bootstrap log:

'; echo '
';

          @ob_flush();
          @flush();

          list($bootstrapOk, $bootstrapLines) = runCommandStreaming($bootstrapCmd, function ($line) {
            echo htmlspecialchars($line) . "\n";
            @ob_flush();
            @flush();
          });

          echo '
'; if ($bootstrapOk) { echo '

Bootstrap completed: standard dev stack applied.

'; } else { echo '

Bootstrap failed: see log above.

'; } } echo '

Back to site list

'; } else { echo '

Site creation failed. See WordOps log above.

'; echo '

Back to site list

'; } echo '
'; } } ?>

Create new site

Cancel
Invalid domain.'; } elseif ( $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm']) && $_POST['confirm'] === 'yes' && (!isset($_POST['op']) || $_POST['op'] !== 'self_change_password') ) { $confirmText = $_POST['confirm_text'] ?? ''; if ($confirmText !== $domain) { echo '
Confirmation text did not match domain.
'; } else { // Enforce ownership for dev users $db = getDb(); $ownerRow = $db->prepare('SELECT owner_id FROM sites WHERE domain = ?'); $ownerRow->execute([$domain]); $ownerId = $ownerRow->fetchColumn(); if (!isAdmin() && (int)$ownerId !== (int)$user['id']) { echo '
You do not have permission to delete this site.
'; } else { $cmd = sprintf( 'sudo %s site delete %s --no-prompt', escapeshellcmd($woPath), escapeshellarg($domain) ); $output = []; $status = null; $ok = runCommand($cmd, $output, $status); echo '
'; if ($ok) { // Remove from metadata $stmt = $db->prepare('DELETE FROM sites WHERE domain = ?'); $stmt->execute([$domain]); echo 'Site deleted: ' . htmlspecialchars($domain) . '
'; } else { echo 'Failed to delete site. Check the log below.'; } echo '
' . htmlspecialchars(implode("\n", $output)) . '
'; echo '

Back to site list

'; echo '
'; } } } else { ?>

Delete site

You're about to delete .

This will remove the vhost and files. Databases are handled by WordOps according to its defaults.

Cancel
prepare('SELECT COUNT(*) FROM users WHERE username = ?'); $stmt->execute([$username]); if ((int)$stmt->fetchColumn() > 0) { $userError = 'Username already exists.'; } else { $hash = password_hash($password, PASSWORD_DEFAULT); $stmt = $db->prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'); $stmt->execute([$username, $hash, $role]); $userMessage = 'User "' . htmlspecialchars($username) . '" created successfully.'; } } } elseif ($op === 'reset_password') { $userId = (int)($_POST['user_id'] ?? 0); if ($userId > 0) { $stmt = $db->prepare('SELECT username FROM users WHERE id = ?'); $stmt->execute([$userId]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { $userError = 'User not found.'; } else { $newPass = generatePassword(16); $hash = password_hash($newPass, PASSWORD_DEFAULT); $update = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); $update->execute([$hash, $userId]); $generatedPasswordInfo = [ 'username' => $row['username'], 'password' => $newPass, ]; $userMessage = 'Password reset for user "' . htmlspecialchars($row['username']) . '".'; } } else { $userError = 'Invalid user ID.'; } } } // Fetch users for listing $stmt = $db->query('SELECT id, username, role FROM users ORDER BY username ASC'); $users = $stmt->fetchAll(PDO::FETCH_ASSOC); ?>

User Management

' . $userMessage . '

'; } if ($userError) { echo '

' . htmlspecialchars($userError) . '

'; } if ($generatedPasswordInfo) { echo '

New password for ' . htmlspecialchars($generatedPasswordInfo['username']) . ': '; echo '' . htmlspecialchars($generatedPasswordInfo['password']) . '

'; } ?>

Existing users

No users found.

Username Role Actions

Create new user

'; echo '

Sites

'; if (!$ok) { echo 'Could not retrieve site list.'; echo '
' . htmlspecialchars(implode("\n", $output)) . '
'; } else { $db = getDb(); // Map of domain => [owner_id, username, role] $meta = []; $stmt = $db->query(' SELECT s.domain, s.owner_id, u.username, u.role FROM sites s LEFT JOIN users u ON s.owner_id = u.id '); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $meta[$row['domain']] = $row; } $sites = []; foreach ($output as $line) { $line = trim($line); // Skip header / separator lines and the panel site itself if ( $line === '' || strpos($line, 'Site') !== false || strpos($line, 'site') !== false || strpos($line, '---') !== false || $line === 'dev-panel.local' ) { continue; } // Take the first column as the domain $parts = preg_split('/\s+/', $line); if (!$parts || !isset($parts[0])) { continue; } $domain = sanitizeDomain($parts[0]); if (!$domain) { continue; } $ownerInfo = $meta[$domain] ?? null; // Role-based filtering: dev sees only their own; admin sees all if (!isAdmin()) { if (!$ownerInfo || (int)$ownerInfo['owner_id'] !== (int)$user['id']) { continue; } } $sites[] = [ 'domain' => $domain, 'owner' => $ownerInfo['username'] ?? null, 'ownerRole' => $ownerInfo['role'] ?? null, 'ownerId' => $ownerInfo['owner_id'] ?? null, ]; } if (empty($sites) || (count($sites) === 1 && strPos($sites[0]['domain'], 'dev-panel') !== false)) { echo '

No sites found for your account yet. Create your first site.

'; } else { echo ''; } } echo ''; } ?>