<?php
// Force login + disable caching
session_start();
require_once __DIR__ . '/../core/db_config.php';
// Anti-cache headers
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Expires: 0');
// Redirect if not logged in
if (empty($_SESSION['username'])) {
$redirect = urlencode($_SERVER['REQUEST_URI'] ?? '/');
header("Location: /core/auth/login.php?redirect={$redirect}");
exit;
}
// Handle SFTP API requests
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sftp_action'])) {
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ob_start();
// Clear any stale connections on new request
if (!isset($_SESSION['sftp_last_activity'])) {
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
} elseif (time() - $_SESSION['sftp_last_activity'] > 3600) {
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
}
class SFTPConnector {
private $connection;
private $sftp;
public function __construct() {
if (!extension_loaded('ssh2')) {
throw new Exception('SSH2 extension is not installed');
}
}
public function connect($host, $port, $username, $password) {
try {
$this->connection = ssh2_connect($host, $port);
if (!$this->connection) {
throw new Exception("Could not connect to $host on port $port");
}
if (!ssh2_auth_password($this->connection, $username, $password)) {
throw new Exception("Authentication failed for user $username");
}
$this->sftp = ssh2_sftp($this->connection);
if (!$this->sftp) {
throw new Exception("Could not initialize SFTP subsystem");
}
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
public function listDirectory($path = '.') {
try {
$handle = opendir("ssh2.sftp://{$this->sftp}$path");
if (!$handle) {
throw new Exception("Could not open directory: $path");
}
$files = [];
while (($file = readdir($handle)) !== false) {
if ($file !== '.' && $file !== '..') {
$fullPath = rtrim($path, '/') . '/' . $file;
$stat = ssh2_sftp_stat($this->sftp, $fullPath);
if ($stat !== false) {
$files[] = [
'name' => $file,
'path' => $fullPath,
'size' => $stat['size'],
'modified' => date('Y-m-d H:i:s', $stat['mtime']),
'type' => is_dir("ssh2.sftp://{$this->sftp}$fullPath") ? 'directory' : 'file'
];
}
}
}
closedir($handle);
usort($files, function($a, $b) {
if ($a['type'] === 'directory' && $b['type'] === 'file') return -1;
if ($a['type'] === 'file' && $b['type'] === 'directory') return 1;
return strcasecmp($a['name'], $b['name']);
});
return $files;
} catch (Exception $e) {
return false;
}
}
public function deleteFile($remotePath) {
try {
if (ssh2_sftp_unlink($this->sftp, $remotePath)) {
return true;
}
return "Failed to delete file";
} catch (Exception $e) {
return $e->getMessage();
}
}
public function createDirectory($path) {
try {
if (ssh2_sftp_mkdir($this->sftp, $path, 0755, true)) {
return true;
}
return "Failed to create directory";
} catch (Exception $e) {
return $e->getMessage();
}
}
public function createFile($path, $content) {
try {
$stream = fopen("ssh2.sftp://{$this->sftp}$path", 'w');
if (!$stream) {
throw new Exception("Could not create file");
}
if (fwrite($stream, $content) === false) {
throw new Exception("Could not write to file");
}
fclose($stream);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
public function disconnect() {
$this->connection = null;
$this->sftp = null;
}
}
class SystemSFTPConnector {
private $host;
private $port;
private $username;
private $password;
private $connected = false;
public function connect($host, $port, $username, $password) {
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->password = $password;
if (!function_exists('exec')) {
return 'System commands are disabled on this server';
}
$testCommand = "echo 'quit' | sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$port} {$username}@{$host} 2>&1";
$output = [];
$returnCode = null;
exec($testCommand, $output, $returnCode);
if ($returnCode === 0 || strpos(implode(' ', $output), 'Connected to') !== false) {
$this->connected = true;
return true;
}
return 'Cannot connect to SFTP server: ' . implode(' ', $output);
}
public function listDirectory($path = '/') {
if (!$this->connected) {
return false;
}
$scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script');
$script = "cd {$path}\nls -la\nquit\n";
file_put_contents($scriptFile, $script);
$command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1";
$output = [];
exec($command, $output);
unlink($scriptFile);
$files = [];
$inListing = false;
foreach ($output as $line) {
if (strpos($line, 'sftp>') !== false && strpos($line, 'ls') !== false) {
$inListing = true;
continue;
}
if ($inListing && strpos($line, 'sftp>') !== false) {
break;
}
if ($inListing && preg_match('/^([drwx-]+)\s+\d+\s+\w+\s+\w+\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$/', $line, $matches)) {
$name = $matches[4];
if ($name !== '.' && $name !== '..') {
$files[] = [
'name' => $name,
'path' => rtrim($path, '/') . '/' . $name,
'size' => (int)$matches[2],
'modified' => $matches[3],
'type' => substr($matches[1], 0, 1) === 'd' ? 'directory' : 'file'
];
}
}
}
return $files;
}
public function deleteFile($remotePath) {
if (!$this->connected) {
return 'Not connected';
}
$scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script');
$script = "rm {$remotePath}\nquit\n";
file_put_contents($scriptFile, $script);
$command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1";
$output = [];
exec($command, $output);
unlink($scriptFile);
if (strpos(implode(' ', $output), 'Removing') !== false || empty(array_filter($output))) {
return true;
}
return 'Delete failed: ' . implode(' ', $output);
}
public function createDirectory($path) {
if (!$this->connected) {
return 'Not connected';
}
$scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script');
$script = "mkdir {$path}\nquit\n";
file_put_contents($scriptFile, $script);
$command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1";
$output = [];
exec($command, $output);
unlink($scriptFile);
return true;
}
public function createFile($path, $content) {
if (!$this->connected) {
return 'Not connected';
}
$tempFile = tempnam(sys_get_temp_dir(), 'sftp_upload');
file_put_contents($tempFile, $content);
$scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script');
$script = "put {$tempFile} {$path}\nquit\n";
file_put_contents($scriptFile, $script);
$command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1";
$output = [];
exec($command, $output);
unlink($scriptFile);
unlink($tempFile);
return true;
}
public function disconnect() {
$this->connected = false;
}
}
$action = $_POST['sftp_action'] ?? '';
$response = ['success' => false, 'message' => '', 'data' => null];
try {
if (extension_loaded('ssh2')) {
$connector = new SFTPConnector();
$connectorType = 'SSH2 Extension';
} else if (function_exists('exec')) {
$connector = new SystemSFTPConnector();
$connectorType = 'System Commands';
} else {
throw new Exception('Neither SSH2 extension nor system commands are available');
}
switch ($action) {
case 'connect':
$result = $connector->connect(
$_POST['host'] ?? '',
$_POST['port'] ?? 22,
$_POST['username'] ?? '',
$_POST['password'] ?? ''
);
if ($result === true) {
$_SESSION['sftp_connected'] = true;
$_SESSION['sftp_config'] = [
'host' => $_POST['host'],
'port' => $_POST['port'] ?? 22,
'username' => $_POST['username'],
'password' => $_POST['password']
];
$_SESSION['sftp_last_activity'] = time();
$response['success'] = true;
$response['message'] = "Connected successfully via {$connectorType}";
} else {
$response['message'] = $result;
}
break;
case 'list':
if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) {
$response['success'] = false;
$response['message'] = 'Not connected to SFTP server. Please connect first.';
break;
}
$config = $_SESSION['sftp_config'];
if (extension_loaded('ssh2')) {
$connector = new SFTPConnector();
} else {
$connector = new SystemSFTPConnector();
}
$connectResult = $connector->connect(
$config['host'],
$config['port'],
$config['username'],
$config['password']
);
if ($connectResult !== true) {
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
$response['message'] = 'Session expired. Please reconnect.';
break;
}
$_SESSION['sftp_last_activity'] = time();
$files = $connector->listDirectory($_POST['path'] ?? '/');
if ($files !== false) {
$response['success'] = true;
$response['data'] = $files;
} else {
$response['message'] = 'Failed to list directory';
}
break;
case 'delete':
if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) {
$response['message'] = 'Not connected to SFTP server';
break;
}
$config = $_SESSION['sftp_config'];
if (extension_loaded('ssh2')) {
$connector = new SFTPConnector();
} else {
$connector = new SystemSFTPConnector();
}
$connectResult = $connector->connect(
$config['host'],
$config['port'],
$config['username'],
$config['password']
);
if ($connectResult !== true) {
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
$response['message'] = 'Session expired. Please reconnect.';
break;
}
$_SESSION['sftp_last_activity'] = time();
$result = $connector->deleteFile($_POST['path'] ?? '');
if ($result === true) {
$response['success'] = true;
$response['message'] = 'File deleted successfully';
} else {
$response['message'] = $result;
}
break;
case 'create_folder':
if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) {
$response['message'] = 'Not connected to SFTP server';
break;
}
$config = $_SESSION['sftp_config'];
if (extension_loaded('ssh2')) {
$connector = new SFTPConnector();
} else {
$connector = new SystemSFTPConnector();
}
$connectResult = $connector->connect(
$config['host'],
$config['port'],
$config['username'],
$config['password']
);
if ($connectResult !== true) {
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
$response['message'] = 'Session expired. Please reconnect.';
break;
}
$_SESSION['sftp_last_activity'] = time();
$result = $connector->createDirectory($_POST['path'] ?? '');
if ($result === true) {
$response['success'] = true;
$response['message'] = 'Directory created successfully';
} else {
$response['message'] = $result;
}
break;
case 'create_file':
if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) {
$response['message'] = 'Not connected to SFTP server';
break;
}
$config = $_SESSION['sftp_config'];
if (extension_loaded('ssh2')) {
$connector = new SFTPConnector();
} else {
$connector = new SystemSFTPConnector();
}
$connectResult = $connector->connect(
$config['host'],
$config['port'],
$config['username'],
$config['password']
);
if ($connectResult !== true) {
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
$response['message'] = 'Session expired. Please reconnect.';
break;
}
$_SESSION['sftp_last_activity'] = time();
$result = $connector->createFile($_POST['path'] ?? '', $_POST['content'] ?? '');
if ($result === true) {
$response['success'] = true;
$response['message'] = 'File created successfully';
} else {
$response['message'] = $result;
}
break;
case 'disconnect':
unset($_SESSION['sftp_connected']);
unset($_SESSION['sftp_config']);
unset($_SESSION['sftp_last_activity']);
if (isset($connector)) {
$connector->disconnect();
}
$response['success'] = true;
$response['message'] = 'Disconnected successfully';
break;
case 'status':
$response['success'] = true;
$response['data'] = [
'connected' => isset($_SESSION['sftp_connected']) && $_SESSION['sftp_connected'],
'config' => isset($_SESSION['sftp_config']) ? [
'host' => $_SESSION['sftp_config']['host'],
'username' => $_SESSION['sftp_config']['username'],
'port' => $_SESSION['sftp_config']['port']
] : null,
'last_activity' => $_SESSION['sftp_last_activity'] ?? null
];
break;
default:
$response['message'] = 'Invalid action: ' . $action;
}
} catch (Exception $e) {
$response['message'] = 'Server error: ' . $e->getMessage();
}
ob_clean();
echo json_encode($response);
exit;
}
// Cache-busting helper
function asset($path) {
$isAbsolute = strlen($path) && $path[0] === '/';
$abs = $isAbsolute ? rtrim($_SERVER['DOCUMENT_ROOT'], '/') . $path : __DIR__ . '/' . $path;
$v = is_file($abs) ? filemtime($abs) : time();
return $path . '?v=' . $v;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SFTP Manager</title>
<link rel="stylesheet" href="<?= asset('/core/css/overlay.css') ?>">
<style>
/* Main Styles Start */
html, body { height: 100%; margin: 0; overscroll-behavior-y: contain; }
body {
background: #0b0f14;
color: #e6edf3;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial;
}
.topbar {
position: sticky; top: 0; z-index: 5;
background: linear-gradient(180deg, rgba(11,15,20,.95), rgba(11,15,20,.85));
border-bottom: 1px solid #1e2633;
backdrop-filter: blur(6px);
}
.topbar-inner {
max-width: 1400px;
margin: 0 auto;
padding: .75rem;
display: flex;
align-items: center;
gap: .75rem;
}
#buttonRow {
flex: 1 1 auto;
min-width: 0;
display: flex;
gap: .75rem;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
#menuContainer { flex: 0 0 auto; margin-left: .25rem; position: relative; }
.chip {
flex: 0 0 auto;
border: 1px solid #2a3648;
background: #1a2332;
color: #e6edf3;
padding: .55rem .9rem;
border-radius: 999px;
font-weight: 600;
cursor: pointer;
transition: background .15s ease;
}
.chip:hover { background: #263244; }
.container {
max-width: 1400px;
margin: 0 auto;
padding: 1.25rem .75rem;
}
.menu-trigger { width: 38px; text-align: center; }
.menu-list {
display: none;
position: absolute;
right: 0; top: calc(100% + 6px);
background: #1a2332;
border: 1px solid #2a3648;
border-radius: 10px;
min-width: 180px;
padding: .25rem 0;
z-index: 9999;
box-shadow: 0 10px 30px rgba(0,0,0,.3);
}
.menu-list.open { display: block; }
.menu-item {
display: block;
width: 100%;
text-align: left;
background: none;
border: none;
color: #e6edf3;
padding: .6rem 1rem;
cursor: pointer;
font: inherit;
}
.menu-item:hover { background: #263244; }
/* Quick Connect Card */
.quick-connect-card {
position: relative;
background: rgba(30, 41, 59, 0.7);
border: 2px solid #2a3648;
border-radius: 12px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
max-width: 600px;
margin: 0 auto;
}
.quick-connect-card.minimized {
padding: 0.75rem 1rem;
border-radius: 8px;
max-width: 100%;
}
.quick-connect-card.minimized .quick-connect-info,
.quick-connect-card.minimized #quickLoginForm {
display: none;
}
.quick-connect-card.minimized .quick-connect-header {
margin: 0;
}
.quick-connect-card.minimized .quick-connect-title {
font-size: 14px;
}
.quick-connect-card.minimized .quick-status-badge {
margin-top: 0;
margin-left: 0.5rem;
}
.quick-connect-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2);
}
.quick-connect-card.active {
border-color: #10b981;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05));
}
.quick-status-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
border-radius: 12px 12px 0 0;
background: #2a3648;
}
.quick-connect-card.active .quick-status-bar {
background: linear-gradient(90deg, #10b981, #34d399);
}
.quick-connect-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
margin-bottom: 12px;
}
.quick-connect-title {
font-size: 18px;
font-weight: 700;
color: #e6edf3;
}
.quick-connect-info {
font-size: 14px;
color: #9aa4b2;
line-height: 1.6;
margin-bottom: 12px;
}
.quick-info-row {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.quick-info-label {
font-weight: 600;
min-width: 60px;
}
.quick-password-input {
width: 100%;
padding: 0.625rem 0.75rem;
background: #0f1725;
border: 1px solid #2a3648;
border-radius: 8px;
color: #e6edf3;
font-size: 0.875rem;
margin-bottom: 8px;
transition: border-color 0.15s;
}
.quick-password-input:focus {
outline: none;
border-color: #3b82f6;
}
.quick-connect-btn {
width: 100%;
padding: 0.625rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.quick-connect-btn:hover {
background: linear-gradient(135deg, #059669, #047857);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.quick-connect-btn:active {
transform: translateY(0);
}
.quick-status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.quick-status-badge.active {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.quick-status-badge.inactive {
background: rgba(148, 163, 184, 0.2);
color: #94a3b8;
}
/* Active Files Bar */
.active-files-bar {
background: rgba(15, 23, 37, 0.8);
border: 1px solid #2a3648;
border-radius: 8px;
padding: 0.5rem;
margin-top: 1rem;
display: none;
overflow-x: auto;
scrollbar-width: thin;
}
.active-files-inner {
display: flex;
gap: 0.25rem;
min-height: 2rem;
}
.active-file-tab {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(30, 41, 59, 0.5);
border: 1px solid #2a3648;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
font-size: 0.875rem;
color: #94a3b8;
}
.active-file-tab:hover {
background: rgba(30, 41, 59, 0.8);
border-color: #3b82f6;
}
.active-file-tab.active {
background: rgba(59, 130, 246, 0.2);
border-color: #3b82f6;
color: #e6edf3;
}
.active-file-close {
background: transparent;
border: none;
color: #9aa4b2;
cursor: pointer;
font-size: 1.25rem;
line-height: 1;
padding: 0;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.15s;
}
.active-file-close:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
/* Projects Section */
.projects-section {
background: rgba(15, 23, 37, 0.5);
border: 1px solid #2a3648;
border-radius: 12px;
padding: 1.5rem;
margin-top: 1.5rem;
display: none;
}
.projects-section.visible {
display: block;
}
.projects-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.projects-header h2 {
margin: 0;
font-size: 1.5rem;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.project-card {
position: relative;
background: rgba(30, 41, 59, 0.7);
border: 2px solid #2a3648;
border-radius: 12px;
padding: 1.25rem;
cursor: pointer;
transition: all 0.2s ease;
}
.project-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2);
}
.project-card-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
}
.project-card-name {
font-size: 1.125rem;
font-weight: 700;
color: #e6edf3;
margin-bottom: 0.5rem;
}
.project-card-path {
font-size: 0.8125rem;
color: #64748b;
}
.file-card {
position: relative;
background: rgba(30, 41, 59, 0.7);
border: 2px solid #2a3648;
border-radius: 12px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 1rem;
}
.file-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2);
}
.file-card-icon {
font-size: 2rem;
flex-shrink: 0;
}
.file-card-info {
flex: 1;
min-width: 0;
}
.file-card-name {
font-size: 0.9375rem;
font-weight: 600;
color: #e6edf3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-card-meta {
font-size: 0.8125rem;
color: #64748b;
margin-top: 0.25rem;
}
.file-card-actions {
display: flex;
gap: 0.25rem;
}
.file-icon-btn {
background: transparent;
border: none;
color: #9aa4b2;
cursor: pointer;
padding: 0.5rem;
border-radius: 6px;
font-size: 1.125rem;
transition: all 0.15s;
}
.file-icon-btn:hover {
background: #263244;
color: #e6edf3;
}
.new-project-card {
background: transparent;
border: 2px dashed #2a3648;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 150px;
color: #9aa4b2;
font-weight: 600;
}
.new-project-card:hover {
border-color: #3b82f6;
color: #3b82f6;
}
.new-project-icon {
font-size: 2rem;
}
.projects-loading {
text-align: center;
padding: 3rem;
color: #64748b;
}
.conn-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
font-size: 14px;
}
.conn-btn-primary {
background: linear-gradient(135deg, #3b82f6, #9333ea);
color: white;
}
.conn-btn-primary:hover {
opacity: 0.9;
}
.conn-toast-container {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 999999;
display: flex;
flex-direction: column;
gap: 8px;
}
.conn-toast {
background: rgba(15, 23, 42, 0.98);
border: 1px solid rgba(71, 85, 105, 0.5);
color: #e2e8f0;
padding: 10px 14px;
border-radius: 10px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
font-size: 0.9rem;
opacity: 0;
transform: translateY(8px);
animation: connToastIn 200ms ease forwards;
}
.conn-toast.success {
border-color: rgba(16, 185, 129, 0.6);
}
.conn-toast.error {
border-color: rgba(239, 68, 68, 0.6);
}
@keyframes connToastIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes connToastOut {
to {
opacity: 0;
transform: translateY(8px);
}
}
</style>
</head>
<body>
<header class="topbar" aria-label="Top navigation">
<div class="topbar-inner">
<div id="buttonRow" role="tablist" aria-label="App sections"></div>
<div id="menuContainer" aria-label="More actions"></div>
</div>
</header>
<main class="container">
<!-- Quick Connect Card -->
<div class="quick-connect-card" id="quickConnectCard">
<div class="quick-status-bar"></div>
<div class="quick-connect-header">
<div class="quick-connect-title">⚡ DevBrewing SFTP</div>
<span class="quick-status-badge inactive" id="quickStatusBadge">⚫ Disconnected</span>
</div>
<div class="quick-connect-info">
<div class="quick-info-row">
<span class="quick-info-label">User:</span>
<span><?= htmlspecialchars($_SESSION['username']) ?></span>
</div>
</div>
<form id="quickLoginForm">
<input
type="password"
class="quick-password-input"
id="quickPassword"
placeholder="Enter SFTP password"
required
>
<button type="submit" class="quick-connect-btn">🚀 Connect</button>
</form>
</div>
<!-- Active Files Bar -->
<div class="active-files-bar" id="activeFilesBar">
<div class="active-files-inner" id="activeFilesInner">
<!-- Active files tabs will appear here -->
</div>
</div>
<!-- Projects Section -->
<div class="projects-section" id="projectsSection">
<div class="projects-header">
<h2>📁 Browse Files</h2>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<select id="categorySelect" onchange="switchTab(this.value)" style="padding: 0.5rem 1rem; background: #1a2332; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-weight: 600; cursor: pointer;">
<option value="projects">📁 Projects</option>
<option value="core">⚙️ Core</option>
<option value="libraries">📚 Libraries</option>
<option value="images">🖼️ Images</option>
<option value="jsons">📋 JSONs</option>
<option value="audios">🎵 Audios</option>
</select>
<button class="conn-btn conn-btn-primary" onclick="handleCreateNewFolder()">+ New Folder</button>
</div>
</div>
<div id="projectsGrid" class="projects-grid">
<div class="projects-loading">Loading...</div>
</div>
</div>
<!-- Folder Contents Section -->
<div class="projects-section" id="projectContentsSection" style="display: none;">
<div class="projects-header">
<button class="conn-btn conn-btn-primary" onclick="handleBackToProjects()">⬅️ Back</button>
<h2 id="currentProjectName">Folder Files</h2>
<div></div>
</div>
<div id="projectContentsGrid" class="projects-grid">
<div class="projects-loading">Loading files...</div>
</div>
</div>
</main>
<div class="conn-toast-container" id="connToastContainer"></div>
<script>
window.AppItems = [];
window.AppMenu = [];
</script>
<script src="/core/js/core.js" defer></script>
<script src="<?= asset('/core/js/overlay.js') ?>" defer></script>
<script src="<?= asset('filemanager.js') ?>" defer></script>
<script src="<?= asset('menu.js') ?>" defer></script>
<script>
// Global state
const STORAGE_KEY = 'sftp_quick_connection';
let currentTab = 'projects';
const TAB_FOLDERS = {
projects: '/files/projects',
core: '/files/core',
libraries: '/files/libraries',
images: '/files/images',
jsons: '/files/jsons',
audios: '/files/audios'
};
const TAB_LABELS = {
projects: 'Projects',
core: 'Core',
libraries: 'Libraries',
images: 'Images',
jsons: 'JSONs',
audios: 'Audios'
};
function getConnection() {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : null;
}
function saveConnection(connection) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(connection));
}
function showToast(message, type = 'success', timeout = 2500) {
const container = document.getElementById('connToastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `conn-toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'connToastOut 160ms ease forwards';
setTimeout(() => container.removeChild(toast), 170);
}, timeout);
}
function updateQuickConnectStatus(isActive) {
const card = document.getElementById('quickConnectCard');
const badge = document.getElementById('quickStatusBadge');
if (isActive) {
card.classList.add('active');
badge.className = 'quick-status-badge active';
badge.textContent = '🟢 Connected';
} else {
card.classList.remove('active');
badge.className = 'quick-status-badge inactive';
badge.textContent = '⚫ Disconnected';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ========== ACTIVE FILES SYSTEM ==========
const ACTIVE_FILES_KEY = 'sftp_active_files';
let activeFiles = [];
// Load saved files when script starts
try {
const saved = localStorage.getItem(ACTIVE_FILES_KEY);
if (saved) activeFiles = JSON.parse(saved);
} catch (e) {
console.warn('Failed to parse saved active files:', e);
activeFiles = [];
}
function saveActiveFiles() {
try {
localStorage.setItem(ACTIVE_FILES_KEY, JSON.stringify(activeFiles));
} catch (e) {
console.error('Failed to save active files:', e);
}
}
function renderActiveFiles() {
const bar = document.getElementById('activeFilesBar');
const inner = document.getElementById('activeFilesInner');
if (!bar || !inner) return;
inner.innerHTML = '';
if (!activeFiles.length) {
bar.style.display = 'none';
return;
}
bar.style.display = 'block';
activeFiles.forEach((file, index) => {
const tab = document.createElement('div');
tab.className = 'active-file-tab' + (file.active ? ' active' : '');
tab.innerHTML = `
<span>${file.icon || '📄'} ${escapeHtml(file.name)}</span>
<button class="active-file-close" title="Close">×</button>
`;
// Close tab
tab.querySelector('.active-file-close').addEventListener('click', (e) => {
e.stopPropagation();
closeActiveFile(index);
});
// Switch active tab
tab.addEventListener('click', () => {
setActiveFile(index);
});
inner.appendChild(tab);
});
}
function addActiveFile(path, name, icon = '📄', content = '') {
// Avoid duplicates
const existingIndex = activeFiles.findIndex(f => f.path === path);
if (existingIndex !== -1) {
setActiveFile(existingIndex);
return;
}
// Deactivate others
activeFiles.forEach(f => f.active = false);
activeFiles.push({ path, name, icon, content, active: true });
saveActiveFiles();
renderActiveFiles();
}
function setActiveFile(index) {
activeFiles.forEach(f => f.active = false);
if (activeFiles[index]) activeFiles[index].active = true;
saveActiveFiles();
renderActiveFiles();
showToast(`Opened ${activeFiles[index].name}`, 'success');
}
function closeActiveFile(index) {
activeFiles.splice(index, 1);
if (activeFiles.length > 0) {
activeFiles[activeFiles.length - 1].active = true;
}
saveActiveFiles();
renderActiveFiles();
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
renderActiveFiles();
});
// Load folders from current tab
async function handleLoadProjects() {
const projectsSection = document.getElementById('projectsSection');
const projectsGrid = document.getElementById('projectsGrid');
if (!projectsSection || !projectsGrid) {
console.error('Projects section not found in DOM');
return;
}
projectsSection.classList.add('visible');
projectsGrid.innerHTML = '<div class="projects-loading">Loading...</div>';
try {
const folderPath = TAB_FOLDERS[currentTab];
console.log('Loading folders from:', folderPath);
// First, ensure the folder exists
await ensureFolderExists(folderPath);
const formData = new FormData();
formData.append('sftp_action', 'list');
formData.append('path', folderPath);
const res = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await res.json();
console.log('Load response:', data);
if (data.success && data.data) {
const folders = data.data.filter(f => f.type === 'directory');
console.log('Found folders:', folders);
renderProjects(folders);
} else {
projectsGrid.innerHTML = '<div class="projects-loading">No folders found</div>';
}
} catch (err) {
console.error('Load error:', err);
projectsGrid.innerHTML = `<div class="projects-loading" style="color: #ef4444;">Error: ${err.message}</div>`;
}
}
async function ensureFolderExists(path) {
try {
const formData = new FormData();
formData.append('sftp_action', 'create_folder');
formData.append('path', path);
await fetch(window.location.href, { method: 'POST', body: formData });
} catch (err) {
// Folder might already exist, that's fine
}
}
function switchTab(tab) {
currentTab = tab;
document.getElementById('categorySelect').value = tab;
handleLoadProjects();
}
function renderProjects(folders) {
const projectsGrid = document.getElementById('projectsGrid');
projectsGrid.innerHTML = '';
// Existing folders
folders.forEach(folder => {
const card = document.createElement('div');
card.className = 'project-card';
card.innerHTML = `
<div class="project-card-icon">📁</div>
<div class="project-card-name">${escapeHtml(folder.name)}</div>
<div class="project-card-path">${TAB_FOLDERS[currentTab]}/${folder.name}</div>
`;
card.onclick = () => handleOpenProject(folder.name);
projectsGrid.appendChild(card);
});
if (folders.length === 0) {
projectsGrid.innerHTML = `<div class="projects-loading">No folders yet. Click "+ New Folder" to create one.</div>`;
}
}
async function handleCreateNewFolder() {
const folderName = prompt(`Enter folder name for ${TAB_LABELS[currentTab]}:`);
if (!folderName) return;
if (!/^[a-zA-Z0-9_-]+$/.test(folderName)) {
showToast('❌ Invalid name. Use letters, numbers, hyphens, underscores only', 'error');
return;
}
try {
const folderPath = `${TAB_FOLDERS[currentTab]}/${folderName}`;
// Create folder
let formData = new FormData();
formData.append('sftp_action', 'create_folder');
formData.append('path', folderPath);
let res = await fetch(window.location.href, { method: 'POST', body: formData });
let data = await res.json();
if (!data.success) throw new Error(data.message);
// Only create starter files for projects
if (currentTab === 'projects') {
// Create index.html
formData = new FormData();
formData.append('sftp_action', 'create_file');
formData.append('path', `${folderPath}/index.html`);
formData.append('content', `<!DOCTYPE html>\n<html>\n<head>\n <title>${folderName}</title>\n</head>\n<body>\n <h1>Welcome to ${folderName}</h1>\n</body>\n</html>`);
res = await fetch(window.location.href, { method: 'POST', body: formData });
data = await res.json();
if (!data.success) throw new Error(data.message);
// Create manifest.json
formData = new FormData();
formData.append('sftp_action', 'create_file');
formData.append('path', `${folderPath}/manifest.json`);
formData.append('content', JSON.stringify({ name: folderName, version: '1.0.0', created: new Date().toISOString() }, null, 2));
res = await fetch(window.location.href, { method: 'POST', body: formData });
data = await res.json();
if (!data.success) throw new Error(data.message);
// Create manifest.db
formData = new FormData();
formData.append('sftp_action', 'create_file');
formData.append('path', `${folderPath}/manifest.db`);
formData.append('content', '');
res = await fetch(window.location.href, { method: 'POST', body: formData });
data = await res.json();
if (!data.success) throw new Error(data.message);
}
showToast(`✅ Folder "${folderName}" created`, 'success');
handleLoadProjects();
} catch (err) {
showToast(`❌ Failed: ${err.message}`, 'error');
}
}
function handleOpenProject(folderName) {
document.getElementById('projectsSection').style.display = 'none';
document.getElementById('projectContentsSection').style.display = 'block';
document.getElementById('currentProjectName').textContent = `📁 ${folderName}`;
loadProjectContents(folderName);
}
function handleBackToProjects() {
document.getElementById('projectContentsSection').style.display = 'none';
document.getElementById('projectsSection').style.display = 'block';
}
async function loadProjectContents(projectName) {
const grid = document.getElementById('projectContentsGrid');
grid.innerHTML = '<div class="projects-loading">Loading files...</div>';
try {
const formData = new FormData();
formData.append('sftp_action', 'list');
formData.append('path', `/files/projects/${projectName}`);
const res = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success && data.data) {
renderProjectContents(data.data, projectName);
} else {
grid.innerHTML = '<div class="projects-loading">No files found</div>';
}
} catch (err) {
grid.innerHTML = `<div class="projects-loading" style="color: #ef4444;">Error: ${err.message}</div>`;
}
}
function renderProjectContents(files, projectName) {
const grid = document.getElementById('projectContentsGrid');
grid.innerHTML = '';
files.forEach(file => {
const card = document.createElement('div');
card.className = 'file-card';
const isDir = file.type === 'directory';
const icon = isDir ? '📁' : getFileIcon(file.name);
const size = isDir ? 'Folder' : formatBytes(file.size);
const fullPath = `/files/projects/${projectName}/${file.name}`;
card.innerHTML = `
<div class="file-card-icon">${icon}</div>
<div class="file-card-info">
<div class="file-card-name">${escapeHtml(file.name)}</div>
<div class="file-card-meta">${size} • ${file.modified || ''}</div>
</div>
${!isDir ? `
<div class="file-card-actions">
<button class="file-icon-btn" onclick="event.stopPropagation(); handleDownloadFile('${fullPath}', '${escapeHtml(file.name)}')" title="Download">⬇️</button>
<button class="file-icon-btn" onclick="event.stopPropagation(); handleDeleteFile('${fullPath}', '${projectName}')" title="Delete">🗑️</button>
</div>
` : ''}
`;
if (!isDir) {
card.onclick = () => handleEditFile(fullPath, file.name);
}
grid.appendChild(card);
});
if (files.length === 0) {
grid.innerHTML = '<div class="projects-loading">This folder is empty</div>';
}
}
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const icons = {
'html': '🌐', 'htm': '🌐',
'css': '🎨',
'js': '⚙️',
'json': '📋',
'md': '📝',
'txt': '📝',
'pdf': '📄',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
'zip': '📦', 'rar': '📦',
'db': '🗄️'
};
return icons[ext] || '📄';
}
function formatBytes(bytes) {
if (!bytes) return '0 B';
if (bytes < 1024) return bytes + ' B';
const units = ['KB', 'MB', 'GB'];
let u = -1;
do { bytes /= 1024; ++u; } while (bytes >= 1024 && u < units.length - 1);
return bytes.toFixed(1) + ' ' + units[u];
}
async function handleDownloadFile(path, filename) {
showToast('⬇️ Download feature coming soon', 'error');
}
async function handleDeleteFile(path, folderName) {
if (!confirm(`Delete ${path.split('/').pop()}?`)) return;
try {
const formData = new FormData();
formData.append('sftp_action', 'delete');
formData.append('path', path);
const res = await fetch(window.location.href, { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
showToast('✅ File deleted', 'success');
loadProjectContents(folderName);
} else {
throw new Error(data.message);
}
} catch (err) {
showToast(`❌ Delete failed: ${err.message}`, 'error');
}
}
function handleEditFile(path, filename) {
const icon = getFileIcon(filename);
addActiveFile(path, filename, icon);
showToast('✏️ Edit feature coming soon', 'error');
}
// Handle card click for disconnect
document.getElementById('quickConnectCard').addEventListener('click', async function(e) {
const card = document.getElementById('quickConnectCard');
// Only handle disconnect when minimized
if (card.classList.contains('minimized')) {
try {
const formData = new FormData();
formData.append('sftp_action', 'disconnect');
await fetch(window.location.href, { method: 'POST', body: formData });
const conn = getConnection();
if (conn) {
conn.active = false;
saveConnection(conn);
}
updateQuickConnectStatus(false);
card.classList.remove('minimized');
document.getElementById('projectsSection').classList.remove('visible');
showToast('Disconnected', 'success');
} catch (err) {
showToast('Disconnect failed: ' + err.message, 'error');
}
}
});
// Handle form submission
document.getElementById('quickLoginForm').addEventListener('submit', async function(e) {
e.preventDefault();
e.stopPropagation();
const password = document.getElementById('quickPassword').value;
const btn = this.querySelector('button[type="submit"]');
const originalText = btn.textContent;
if (!password) {
showToast('❌ Enter password', 'error');
return;
}
btn.disabled = true;
btn.textContent = '🔄 Connecting...';
try {
const formData = new FormData();
formData.append('sftp_action', 'connect');
formData.append('host', 'files.devbrewing.com');
formData.append('port', 22);
formData.append('username', '<?= htmlspecialchars($_SESSION['username']) ?>');
formData.append('password', password);
const res = await fetch(window.location.href, { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
const conn = {
host: 'files.devbrewing.com',
port: 22,
username: '<?= htmlspecialchars($_SESSION['username']) ?>',
password: password,
active: true
};
saveConnection(conn);
updateQuickConnectStatus(true);
// Minimize and load projects
document.getElementById('quickConnectCard').classList.add('minimized');
handleLoadProjects();
showToast('✅ Connected to DevBrewing', 'success');
} else {
throw new Error(data.message || 'Connection failed');
}
} catch (err) {
showToast(`❌ ${err.message}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
});
// On page load
document.addEventListener('DOMContentLoaded', function() {
const conn = getConnection();
if (conn) {
updateQuickConnectStatus(conn.active || false);
if (conn.password) {
document.getElementById('quickPassword').value = conn.password;
}
if (conn.active) {
document.getElementById('quickConnectCard').classList.add('minimized');
// Wait a bit for DOM to be ready, then load projects
setTimeout(() => {
handleLoadProjects();
}, 300);
}
}
});
// Render menu
document.addEventListener('DOMContentLoaded', () => {
const row = document.getElementById('buttonRow');
row.innerHTML = '';
(window.AppItems || []).forEach((item, i) => {
const btn = document.createElement('button');
btn.className = 'chip';
btn.textContent = item.title || `Item ${i+1}`;
btn.onclick = () => window.AppOverlay && AppOverlay.open(window.AppItems, i, btn);
row.appendChild(btn);
});
const menuContainer = document.getElementById('menuContainer');
menuContainer.innerHTML = '';
const menuItems = window.AppMenu || [];
if (menuItems.length > 0) {
const trigger = document.createElement('button');
trigger.className = 'chip menu-trigger';
trigger.textContent = '⋮';
menuContainer.appendChild(trigger);
const dropdown = document.createElement('div');
dropdown.className = 'menu-list';
menuContainer.appendChild(dropdown);
menuItems.forEach((m, idx) => {
const item = document.createElement('button');
item.className = 'menu-item';
item.textContent = m.label || `Action ${idx+1}`;
item.onclick = () => { dropdown.classList.remove('open'); m.action && m.action(); };
dropdown.appendChild(item);
});
trigger.addEventListener('click', (e)=>{
e.stopPropagation();
dropdown.classList.toggle('open');
});
document.addEventListener('click', (e)=>{
if (!menuContainer.contains(e.target)) dropdown.classList.remove('open');
});
}
});
</script>
</body>
</html>