feat: implement project management system with Nginx configuration templates and automated host file updates
This commit is contained in:
@@ -1,15 +1,8 @@
|
|||||||
# Apache Project Configuration (CGI Mode)
|
<VirtualHost *:{{PORT}}>
|
||||||
# Accessed via: http://localhost:{{PORT}}/{{HOST}}/
|
ServerName {{HOST}}
|
||||||
|
DocumentRoot "{{PATH}}"
|
||||||
|
|
||||||
# 1. Alias definition (restores localhost/projectname access)
|
<Directory "{{PATH}}">
|
||||||
Alias /{{HOST}} "{{PATH}}"
|
|
||||||
|
|
||||||
# 2. ScriptAlias definition for the project's PHP CGI binary
|
|
||||||
# This maps a virtual URL path to the actual PHP folder.
|
|
||||||
ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/"
|
|
||||||
|
|
||||||
# 3. Directory settings for the project
|
|
||||||
<Directory "{{PATH}}">
|
|
||||||
Options Indexes FollowSymLinks ExecCGI
|
Options Indexes FollowSymLinks ExecCGI
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
Require all granted
|
Require all granted
|
||||||
@@ -26,13 +19,15 @@ ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/"
|
|||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
# Action maps the handler to the binary via the ScriptAlias URL
|
# Action maps the handler to the binary via the ScriptAlias URL
|
||||||
# This is more robust on Windows than using absolute local paths.
|
|
||||||
Action application/x-httpd-php-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe"
|
Action application/x-httpd-php-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe"
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# 4. Permissions for the PHP binary directory
|
# ScriptAlias definition for the project's PHP CGI binary
|
||||||
<Directory "{{PHP_DIR}}">
|
ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/"
|
||||||
|
|
||||||
|
<Directory "{{PHP_DIR}}">
|
||||||
AllowOverride None
|
AllowOverride None
|
||||||
Options None
|
Options None
|
||||||
Require all granted
|
Require all granted
|
||||||
</Directory>
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Subdirectory Alias for {{HOST}}
|
||||||
|
Alias /{{HOST}} "{{PATH}}"
|
||||||
|
|
||||||
|
<Directory "{{PATH}}">
|
||||||
|
Options Indexes FollowSymLinks ExecCGI
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# Passing Authorization header to PHP
|
||||||
|
CGIPassAuth On
|
||||||
|
|
||||||
|
# Directory index handling
|
||||||
|
DirectoryIndex index.php index.html
|
||||||
|
|
||||||
|
# PHP Handling via mod_actions and CGI
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
SetHandler application/x-httpd-php-alias-{{HOST}}
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Action maps the handler to the binary via the ScriptAlias URL
|
||||||
|
# We use a unique handler name to avoid conflict with VirtualHost config
|
||||||
|
Action application/x-httpd-php-alias-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe"
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# ScriptAlias definition for the project's PHP CGI binary
|
||||||
|
ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/"
|
||||||
|
|
||||||
|
<Directory "{{PHP_DIR}}">
|
||||||
|
AllowOverride None
|
||||||
|
Options None
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
@@ -83,7 +83,10 @@ Alias /phpmyadmin "{{PHPMYADMIN_DIR}}"
|
|||||||
CGIPassAuth On
|
CGIPassAuth On
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# Project configurations
|
# Project configurations (Subdirectory Access)
|
||||||
|
{{PROJECT_ALIASES}}
|
||||||
|
|
||||||
|
# Project configurations (Virtual Host Access)
|
||||||
{{PROJECTS}}
|
{{PROJECTS}}
|
||||||
|
|
||||||
# Global PHP handler (Manual Nginx-Style SCRIPT_FILENAME)
|
# Global PHP handler (Manual Nginx-Style SCRIPT_FILENAME)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ http {
|
|||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
|
server_names_hash_bucket_size 64;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen {{LISTEN_ADDRESS}}:{{PORT}};
|
listen {{LISTEN_ADDRESS}}:{{PORT}};
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
root "{{ROOT}}";
|
root "{{ROOT}}";
|
||||||
index index.php index.html index.htm;
|
index index.php index.html index.htm;
|
||||||
|
autoindex on;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# Try global root first, fallback to @project_fallback if not found
|
# Try global root first, fallback to @project_fallback if not found
|
||||||
@@ -36,7 +38,9 @@ http {
|
|||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
{{PROJECTS}}
|
# Subdirectory access for projects (legacy support)
|
||||||
|
{{PROJECT_LOCATIONS}}
|
||||||
|
|
||||||
|
|
||||||
location /phpmyadmin {
|
location /phpmyadmin {
|
||||||
alias "{{PHPMYADMIN_DIR}}";
|
alias "{{PHPMYADMIN_DIR}}";
|
||||||
@@ -58,4 +62,6 @@ http {
|
|||||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{PROJECTS}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
location ^~ /{{HOST}}/ {
|
server {
|
||||||
alias "{{PATH}}";
|
listen {{PORT}};
|
||||||
|
server_name {{HOST}};
|
||||||
|
root "{{PATH}}";
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
autoindex on;
|
autoindex on;
|
||||||
|
|
||||||
# Use a named location for reliable alias fallback
|
# Support for clean URLs
|
||||||
try_files $uri $uri/ @{{HOST}};
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
# PHP handling nested INSIDE the alias location to maintain context
|
# PHP handling
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
|
|
||||||
include "{{CONF_DIR}}/fastcgi_params";
|
include "{{CONF_DIR}}/fastcgi_params";
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
# Normal files matched correctly by URI don't suffer the try_files bug
|
|
||||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# The named location handles requests that don't match physical files
|
# Custom log files for the project
|
||||||
location @{{HOST}} {
|
# access_log "{{LOG_DIR}}/{{HOST}}_access.log";
|
||||||
# Directly execute PHP without URI mutation to preserve framework routing paths
|
# error_log "{{LOG_DIR}}/{{HOST}}_error.log";
|
||||||
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
|
||||||
include "{{CONF_DIR}}/fastcgi_params";
|
|
||||||
|
|
||||||
# Hardcode the essential execution parameters
|
|
||||||
fastcgi_param SCRIPT_FILENAME "{{PATH}}index.php";
|
|
||||||
fastcgi_param SCRIPT_NAME "/{{HOST}}/index.php";
|
|
||||||
fastcgi_param DOCUMENT_ROOT "{{PATH}}";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
location ^~ /{{HOST}}/ {
|
||||||
|
alias "{{PATH}}";
|
||||||
|
index index.php index.html;
|
||||||
|
autoindex on;
|
||||||
|
|
||||||
|
if (!-e $request_filename) {
|
||||||
|
rewrite ^/{{HOST}}/(.*)$ /{{HOST}}/index.php?$1 last;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
# Using $request_filename to match the alias path correctly
|
||||||
|
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
||||||
|
fastcgi_index index.php;
|
||||||
|
include "{{CONF_DIR}}/fastcgi_params";
|
||||||
|
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1653
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"nginxPort": "80",
|
"nginxPort": "8081",
|
||||||
"apachePort": "8080",
|
"apachePort": "8080",
|
||||||
"serverType": "nginx",
|
"serverType": "nginx",
|
||||||
"phpPort": "9001",
|
"phpPort": "9001",
|
||||||
|
|||||||
+102
-26
@@ -1,4 +1,5 @@
|
|||||||
import { ipcMain, app, dialog, shell } from 'electron'
|
import { ipcMain, app, dialog, shell } from 'electron'
|
||||||
|
import { exec } from 'child_process'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { processManager } from '../services/ProcessManager'
|
import { processManager } from '../services/ProcessManager'
|
||||||
import { configService } from '../services/ConfigService'
|
import { configService } from '../services/ConfigService'
|
||||||
@@ -15,6 +16,7 @@ import { mariaDbOperationService } from '../services/MariaDbOperationService'
|
|||||||
import { cliAliasService } from '../services/CliAliasService'
|
import { cliAliasService } from '../services/CliAliasService'
|
||||||
import { cliBinService } from '../services/CliBinService'
|
import { cliBinService } from '../services/CliBinService'
|
||||||
import { serviceEventLogger } from '../services/ServiceEventLogger'
|
import { serviceEventLogger } from '../services/ServiceEventLogger'
|
||||||
|
import { hostsService } from '../services/HostsService'
|
||||||
|
|
||||||
function findExecutable(dir: string, name: string): string | null {
|
function findExecutable(dir: string, name: string): string | null {
|
||||||
if (!fs.existsSync(dir)) return null
|
if (!fs.existsSync(dir)) return null
|
||||||
@@ -44,13 +46,14 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const binPath = path.join(nginxPrefix, isWindows ? 'nginx.exe' : 'nginx')
|
const binPath = path.join(nginxPrefix, isWindows ? 'nginx.exe' : 'nginx')
|
||||||
const confDir = path.join(nginxPrefix, 'conf')
|
const confDir = path.join(nginxPrefix, 'conf')
|
||||||
const logDir = path.join(configService.getBasePath(), 'logs')
|
|
||||||
const rootPath = path.join(configService.getBasePath(), 'www')
|
const rootPath = path.join(configService.getBasePath(), 'www')
|
||||||
if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true })
|
if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true })
|
||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects')
|
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects')
|
||||||
|
const locationsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_locations')
|
||||||
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
|
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
|
||||||
|
if (!fs.existsSync(locationsConfDir)) fs.mkdirSync(locationsConfDir, { recursive: true })
|
||||||
|
|
||||||
const tempDirs = ['temp', 'temp/client_body_temp', 'temp/proxy_temp', 'temp/fastcgi_temp', 'temp/uwsgi_temp', 'temp/scgi_temp', 'logs']
|
const tempDirs = ['temp', 'temp/client_body_temp', 'temp/proxy_temp', 'temp/fastcgi_temp', 'temp/uwsgi_temp', 'temp/scgi_temp', 'logs']
|
||||||
for (const dir of tempDirs) {
|
for (const dir of tempDirs) {
|
||||||
@@ -59,8 +62,15 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate project configs
|
// Generate project configs
|
||||||
const allProjects = await projectService.getProjects()
|
const projects = projectService.getProjects().filter(p => !p.serverType || p.serverType === 'nginx')
|
||||||
const projects = allProjects.filter(p => !p.serverType || p.serverType === 'nginx')
|
|
||||||
|
if (forceProjects) {
|
||||||
|
const files = fs.readdirSync(projectsConfDir)
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.conf')) fs.unlinkSync(path.join(projectsConfDir, file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of projects) {
|
for (const p of projects) {
|
||||||
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
|
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
|
||||||
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
|
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
|
||||||
@@ -82,11 +92,24 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
normalizedPath += '/'
|
normalizedPath += '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-suffix hostname if it's a simple slug
|
||||||
|
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`
|
||||||
|
|
||||||
await configService.generateConfig('nginx_project.conf.template', `projects/${p.host}.conf`, {
|
await configService.generateConfig('nginx_project.conf.template', `projects/${p.host}.conf`, {
|
||||||
HOST: p.host,
|
HOST: finalHost,
|
||||||
PATH: normalizedPath,
|
PATH: normalizedPath,
|
||||||
PHP_PORT: phpPort.toString(),
|
PHP_PORT: phpPort.toString(),
|
||||||
CONF_DIR: confDir.replace(/\\/g, '/')
|
CONF_DIR: confDir.replace(/\\/g, '/'),
|
||||||
|
PORT: settings.nginxPort
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also generate the subdirectory/alias-style config for localhost fallback
|
||||||
|
await configService.generateConfig('nginx_project_location.conf.template', `projects_locations/${p.host}.conf`, {
|
||||||
|
HOST: p.host, // Use simple host slug for location path
|
||||||
|
PATH: normalizedPath,
|
||||||
|
PHP_PORT: phpPort.toString(),
|
||||||
|
CONF_DIR: confDir.replace(/\\/g, '/'),
|
||||||
|
PORT: settings.nginxPort
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,15 +118,20 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
? `include "${projectsConfDir.replace(/\\/g, '/')}/*.conf";`
|
? `include "${projectsConfDir.replace(/\\/g, '/')}/*.conf";`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
const locationsInclusion = projects.length > 0
|
||||||
|
? `include "${locationsConfDir.replace(/\\/g, '/')}/*.conf";`
|
||||||
|
: ''
|
||||||
|
|
||||||
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
|
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
|
||||||
PORT: settings.nginxPort,
|
PORT: settings.nginxPort.toString(),
|
||||||
|
ROOT: rootPath.replace(/\\/g, '/'),
|
||||||
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
|
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
|
||||||
PHP_PORT: settings.phpPort,
|
PHP_PORT: settings.phpPort.toString(),
|
||||||
ROOT: rootPath,
|
LOG_DIR: path.join(nginxPrefix, 'logs').replace(/\\/g, '/'),
|
||||||
LOG_DIR: logDir.replace(/\\/g, '/'),
|
|
||||||
CONF_DIR: confDir.replace(/\\/g, '/'),
|
CONF_DIR: confDir.replace(/\\/g, '/'),
|
||||||
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/'),
|
PHPMYADMIN_DIR: path.join(configService.getBasePath(), 'bin', 'phpmyadmin').replace(/\\/g, '/'),
|
||||||
PROJECTS: projectsInclusion
|
PROJECTS: projectsInclusion,
|
||||||
|
PROJECT_LOCATIONS: locationsInclusion
|
||||||
})
|
})
|
||||||
|
|
||||||
return { binPath, nginxPrefix, configPath }
|
return { binPath, nginxPrefix, configPath }
|
||||||
@@ -111,7 +139,7 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
|
|
||||||
async function restartNginx(): Promise<{ success: boolean; message: string }> {
|
async function restartNginx(): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
const { binPath, nginxPrefix, configPath } = await rebuildNginxConfig(false)
|
const { binPath, nginxPrefix, configPath } = await rebuildNginxConfig(true)
|
||||||
|
|
||||||
// Force kill Nginx to ensure it picks up new config on Windows
|
// Force kill Nginx to ensure it picks up new config on Windows
|
||||||
await processManager.forceKillAll('nginx')
|
await processManager.forceKillAll('nginx')
|
||||||
@@ -156,8 +184,6 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
|
|||||||
const binPath = findExecutable(apacheDir, isWindows ? 'httpd.exe' : 'httpd')
|
const binPath = findExecutable(apacheDir, isWindows ? 'httpd.exe' : 'httpd')
|
||||||
if (!binPath) throw new Error('Apache executable bulunamadı.')
|
if (!binPath) throw new Error('Apache executable bulunamadı.')
|
||||||
|
|
||||||
const apacheRoot = path.dirname(path.dirname(binPath)) // bin'in üstü
|
|
||||||
const logDir = path.join(configService.getBasePath(), 'logs')
|
|
||||||
const rootPath = path.join(configService.getBasePath(), 'www')
|
const rootPath = path.join(configService.getBasePath(), 'www')
|
||||||
if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true })
|
if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true })
|
||||||
|
|
||||||
@@ -175,11 +201,19 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
|
|||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_apache')
|
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_apache')
|
||||||
|
const aliasesConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_aliases_apache')
|
||||||
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
|
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
|
||||||
|
if (!fs.existsSync(aliasesConfDir)) fs.mkdirSync(aliasesConfDir, { recursive: true })
|
||||||
|
|
||||||
// Generate project configs
|
if (forceProjects) {
|
||||||
const allProjects = await projectService.getProjects()
|
const files = fs.readdirSync(projectsConfDir)
|
||||||
const projects = allProjects.filter(p => p.serverType === 'apache')
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.conf')) fs.unlinkSync(path.join(projectsConfDir, file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure projects exist
|
||||||
|
const projects = projectService.getProjects().filter(p => p.serverType === 'apache')
|
||||||
for (const p of projects) {
|
for (const p of projects) {
|
||||||
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
|
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
|
||||||
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
|
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
|
||||||
@@ -200,13 +234,28 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
|
|||||||
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${p.phpVersion}`)
|
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${p.phpVersion}`)
|
||||||
const phpCgiBinary = findExecutable(phpDir, process.platform === 'win32' ? 'php-cgi.exe' : 'php-cgi') || ''
|
const phpCgiBinary = findExecutable(phpDir, process.platform === 'win32' ? 'php-cgi.exe' : 'php-cgi') || ''
|
||||||
|
|
||||||
|
// Auto-suffix hostname if it's a simple slug
|
||||||
|
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`
|
||||||
|
|
||||||
await configService.generateConfig('apache_project.conf.template', `projects_apache/${p.host}.conf`, {
|
await configService.generateConfig('apache_project.conf.template', `projects_apache/${p.host}.conf`, {
|
||||||
|
HOST: finalHost,
|
||||||
|
PATH: normalizedPath,
|
||||||
|
PHP_PORT: phpPort.toString(),
|
||||||
|
PHP_DIR: phpDir.replace(/\\/g, '/'),
|
||||||
|
PHP_CGI_PATH: phpCgiBinary.replace(/\\/g, '/'),
|
||||||
|
PHP_BIN_URL: `/php-bin-${p.host}`,
|
||||||
|
PORT: settings.apachePort || '8080'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also generate the subdirectory/alias-style config for localhost fallback
|
||||||
|
await configService.generateConfig('apache_project_alias.conf.template', `projects_aliases_apache/${p.host}.conf`, {
|
||||||
HOST: p.host,
|
HOST: p.host,
|
||||||
PATH: normalizedPath,
|
PATH: normalizedPath,
|
||||||
PHP_PORT: phpPort.toString(),
|
PHP_PORT: phpPort.toString(),
|
||||||
PHP_DIR: phpDir.replace(/\\/g, '/'),
|
PHP_DIR: phpDir.replace(/\\/g, '/'),
|
||||||
PHP_CGI_PATH: phpCgiBinary.replace(/\\/g, '/'),
|
PHP_CGI_PATH: phpCgiBinary.replace(/\\/g, '/'),
|
||||||
PHP_BIN_URL: `/php-bin-${p.host}`
|
PHP_BIN_URL: `/php-bin-alias-${p.host}`,
|
||||||
|
PORT: settings.apachePort || '8080'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,15 +265,20 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
|
|||||||
? `Include "${projectsConfDir.replace(/\\/g, '/')}/*.conf"`
|
? `Include "${projectsConfDir.replace(/\\/g, '/')}/*.conf"`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
const aliasesInclusion = projects.length > 0
|
||||||
|
? `Include "${aliasesConfDir.replace(/\\/g, '/')}/*.conf"`
|
||||||
|
: ''
|
||||||
|
|
||||||
await configService.generateConfig('httpd.conf.template', 'httpd.conf', {
|
await configService.generateConfig('httpd.conf.template', 'httpd.conf', {
|
||||||
PORT: settings.apachePort || '8080',
|
PORT: settings.apachePort.toString(),
|
||||||
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
|
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
|
||||||
PHP_PORT: settings.phpPort,
|
|
||||||
ROOT: rootPath.replace(/\\/g, '/'),
|
ROOT: rootPath.replace(/\\/g, '/'),
|
||||||
SRVROOT: apacheRoot.replace(/\\/g, '/'),
|
PHP_PORT: settings.phpPort.toString(),
|
||||||
LOG_DIR: logDir.replace(/\\/g, '/'),
|
LOG_DIR: path.join(configService.getBasePath(), 'logs').replace(/\\/g, '/'),
|
||||||
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/'),
|
SRVROOT: path.join(configService.getBasePath(), 'bin', 'apache', 'Apache24').replace(/\\/g, '/'),
|
||||||
PROJECTS: projectsInclusion
|
PHPMYADMIN_DIR: path.join(configService.getBasePath(), 'bin', 'phpmyadmin').replace(/\\/g, '/'),
|
||||||
|
PROJECTS: projectsInclusion,
|
||||||
|
PROJECT_ALIASES: aliasesInclusion
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +287,7 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
|
|||||||
|
|
||||||
async function restartApache(): Promise<{ success: boolean; message: string }> {
|
async function restartApache(): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
const { binPath, configPath } = await rebuildApacheConfig(false)
|
const { binPath, configPath } = await rebuildApacheConfig(true)
|
||||||
const settings = configService.getSettings()
|
const settings = configService.getSettings()
|
||||||
|
|
||||||
await processManager.forceKillAll('apache')
|
await processManager.forceKillAll('apache')
|
||||||
@@ -796,10 +850,32 @@ export function registerIpcHandlers(): void {
|
|||||||
return result.filePath
|
return result.filePath
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('shell:open-path', async (_event, pathStr: string) => {
|
ipcMain.handle('open-path', async (_event, pathStr: string) => {
|
||||||
return await shell.openPath(pathStr)
|
return await shell.openPath(pathStr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('open-vscode', async (_event, pathStr: string) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec(`code "${pathStr}"`, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to open VS Code:', error)
|
||||||
|
resolve({ success: false, message: error.message })
|
||||||
|
} else {
|
||||||
|
resolve({ success: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hosts file management
|
||||||
|
ipcMain.handle('hosts:check', async (_event, hostname: string) => {
|
||||||
|
return await hostsService.checkEntry(hostname)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('hosts:add', async (_event, hostname: string) => {
|
||||||
|
return await hostsService.addEntry(hostname)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||||
const result = await downloadService.downloadAndExtract(url, name, (p) => {
|
const result = await downloadService.downloadAndExtract(url, name, (p) => {
|
||||||
// TODO: Emit progress to renderer via webContents.send
|
// TODO: Emit progress to renderer via webContents.send
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
|
||||||
|
export class HostsService {
|
||||||
|
private hostsPath: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Windows: C:\Windows\System32\drivers\etc\hosts
|
||||||
|
// Linux/macOS: /etc/hosts
|
||||||
|
this.hostsPath = process.platform === 'win32'
|
||||||
|
? path.join(process.env.windir || 'C:\\Windows', 'System32', 'drivers', 'etc', 'hosts')
|
||||||
|
: '/etc/hosts'
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkEntry(hostname: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(this.hostsPath)) return false
|
||||||
|
const content = fs.readFileSync(this.hostsPath, 'utf8')
|
||||||
|
|
||||||
|
// Look for 127.0.0.1 <hostname> or ::1 <hostname>
|
||||||
|
const regex = new RegExp(`(^|\\s)(127\\.0\\.0\\.1|::1)\\s+${hostname.replace(/\./g, '\\.')}(\\s|$)`, 'm')
|
||||||
|
return regex.test(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read hosts file:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addEntry(hostname: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
if (await this.checkEntry(hostname)) {
|
||||||
|
return { success: true, message: 'Entry already exists' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows: Use PowerShell with -Verb RunAs for UAC prompt
|
||||||
|
// Using `n (newline) to ensure we don't append to the last line's end
|
||||||
|
const cmd = `powershell -Command "Start-Process powershell -ArgumentList '-Command Add-Content -Path $env:windir\\System32\\drivers\\etc\\hosts -Value \"\`n127.0.0.1 ${hostname}\" -Force' -Verb RunAs"`
|
||||||
|
|
||||||
|
exec(cmd, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to update hosts file:', error)
|
||||||
|
resolve({ success: false, message: 'Yönetici izni (UAC) reddedildi veya bir hata oluştu.' })
|
||||||
|
} else {
|
||||||
|
// Success doesn't mean the entry was actually added (could have been cancelled in UAC)
|
||||||
|
// So we wait a bit and re-check
|
||||||
|
setTimeout(async () => {
|
||||||
|
const verified = await this.checkEntry(hostname)
|
||||||
|
if (verified) {
|
||||||
|
resolve({ success: true, message: 'Hosts dosyası başarıyla güncellendi.' })
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, message: 'Kayıt eklenemedi. Lütfen yönetici iznini onaylayın.' })
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Linux/macOS: Use sudo
|
||||||
|
const cmd = `echo "127.0.0.1 ${hostname}" | sudo tee -a ${this.hostsPath}`
|
||||||
|
exec(cmd, (error) => {
|
||||||
|
if (error) {
|
||||||
|
resolve({ success: false, message: error.message })
|
||||||
|
} else {
|
||||||
|
resolve({ success: true, message: 'Hosts file updated.' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hostsService = new HostsService()
|
||||||
@@ -268,11 +268,15 @@ export class PhpManagerService {
|
|||||||
const iniPath = path.join(phpDir, 'php.ini')
|
const iniPath = path.join(phpDir, 'php.ini')
|
||||||
if (!fs.existsSync(iniPath)) return false
|
if (!fs.existsSync(iniPath)) return false
|
||||||
|
|
||||||
|
const sessionsDir = path.join(configService.getBasePath(), 'sessions').replace(/\\/g, '/')
|
||||||
|
if (!fs.existsSync(sessionsDir)) fs.mkdirSync(sessionsDir, { recursive: true })
|
||||||
|
|
||||||
let content = fs.readFileSync(iniPath, 'utf8')
|
let content = fs.readFileSync(iniPath, 'utf8')
|
||||||
const lines = content.split(/\r?\n/)
|
const lines = content.split(/\r?\n/)
|
||||||
|
|
||||||
let forceRedirectSet = false
|
let forceRedirectSet = false
|
||||||
let fixPathinfoSet = false
|
let fixPathinfoSet = false
|
||||||
|
let sessionPathSet = false
|
||||||
|
|
||||||
const newLines = lines.map(line => {
|
const newLines = lines.map(line => {
|
||||||
const trimmed = line.trim()
|
const trimmed = line.trim()
|
||||||
@@ -284,11 +288,16 @@ export class PhpManagerService {
|
|||||||
fixPathinfoSet = true
|
fixPathinfoSet = true
|
||||||
return 'cgi.fix_pathinfo = 1'
|
return 'cgi.fix_pathinfo = 1'
|
||||||
}
|
}
|
||||||
|
if (trimmed.startsWith('session.save_path') || trimmed.startsWith(';session.save_path')) {
|
||||||
|
sessionPathSet = true
|
||||||
|
return `session.save_path = "${sessionsDir}"`
|
||||||
|
}
|
||||||
return line
|
return line
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!forceRedirectSet) newLines.push('cgi.force_redirect = 0')
|
if (!forceRedirectSet) newLines.push('cgi.force_redirect = 0')
|
||||||
if (!fixPathinfoSet) newLines.push('cgi.fix_pathinfo = 1')
|
if (!fixPathinfoSet) newLines.push('cgi.fix_pathinfo = 1')
|
||||||
|
if (!sessionPathSet) newLines.push(`session.save_path = "${sessionsDir}"`)
|
||||||
|
|
||||||
fs.writeFileSync(iniPath, newLines.join('\n'), 'utf8')
|
fs.writeFileSync(iniPath, newLines.join('\n'), 'utf8')
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -34,8 +34,18 @@ export class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private saveProjects() {
|
private saveProjects() {
|
||||||
|
const tempFile = `${this.projectsFile}.tmp`
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(tempFile, JSON.stringify(this.projects, null, 2))
|
||||||
|
if (fs.existsSync(tempFile)) {
|
||||||
|
fs.renameSync(tempFile, this.projectsFile)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save projects safely:', e)
|
||||||
|
// Fallback to direct write if rename fails
|
||||||
fs.writeFileSync(this.projectsFile, JSON.stringify(this.projects, null, 2))
|
fs.writeFileSync(this.projectsFile, JSON.stringify(this.projects, null, 2))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getProjects(): Project[] {
|
getProjects(): Project[] {
|
||||||
return this.projects
|
return this.projects
|
||||||
|
|||||||
@@ -350,10 +350,6 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchStatus()
|
fetchStatus()
|
||||||
// fetchProjects() // Reduced frequency for these
|
|
||||||
// fetchPhpVersions()
|
|
||||||
// fetchMariaDbVersions()
|
|
||||||
// fetchNginxVersions()
|
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
let logInterval: NodeJS.Timeout | null = null
|
let logInterval: NodeJS.Timeout | null = null
|
||||||
@@ -1135,8 +1131,6 @@ function App(): JSX.Element {
|
|||||||
{activeTab === 'user_guide' && <UserGuide />}
|
{activeTab === 'user_guide' && <UserGuide />}
|
||||||
{activeTab === 'settings' && (
|
{activeTab === 'settings' && (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
||||||
<Tab label="PHP" icon={<PhpIcon />} iconPosition="start" />
|
<Tab label="PHP" icon={<PhpIcon />} iconPosition="start" />
|
||||||
@@ -1537,8 +1531,6 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
{activeTab === 'dashboard' && (
|
{activeTab === 'dashboard' && (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
<Tabs value={dashboardTab} onChange={(_e, v) => setDashboardTab(v)} textColor="primary" indicatorColor="primary">
|
<Tabs value={dashboardTab} onChange={(_e, v) => setDashboardTab(v)} textColor="primary" indicatorColor="primary">
|
||||||
<Tab label={t('common.all')} />
|
<Tab label={t('common.all')} />
|
||||||
@@ -1986,11 +1978,28 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
}}
|
}}
|
||||||
onOpenSite={(p: any) => {
|
onOpenSite={(p: any, isHostMapped: boolean) => {
|
||||||
const port = p.serverType === 'apache' ? settings.apachePort : settings.nginxPort
|
const port = p.serverType === 'apache' ? settings.apachePort : settings.nginxPort
|
||||||
|
if (isHostMapped) {
|
||||||
|
const finalHost = p.host && p.host.includes('.') ? p.host : `${p.host}.local`
|
||||||
|
window.open(`http://${finalHost}:${port}/`, '_blank')
|
||||||
|
} else {
|
||||||
window.open(`http://localhost:${port}/${p.host}/`, '_blank')
|
window.open(`http://localhost:${port}/${p.host}/`, '_blank')
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onOpenFolder={(path: string) => window.api.invoke('open-path', path)}
|
onOpenFolder={(path: string) => window.api.invoke('open-path', path)}
|
||||||
|
onOpenVSCode={(path: string) => window.api.invoke('open-vscode', path)}
|
||||||
|
onAddHost={async (hostname: string) => {
|
||||||
|
const result = await window.api.invoke('hosts:add', hostname);
|
||||||
|
if (result.success) {
|
||||||
|
// Refresh current project to check mapping again
|
||||||
|
const currentId = selectedProjectId;
|
||||||
|
setSelectedProjectId(null);
|
||||||
|
setTimeout(() => setSelectedProjectId(currentId), 10);
|
||||||
|
} else {
|
||||||
|
alert(result.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onManageDb={() => setActiveTab('db_manager')}
|
onManageDb={() => setActiveTab('db_manager')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import {
|
|||||||
Chip,
|
Chip,
|
||||||
Divider,
|
Divider,
|
||||||
Stack,
|
Stack,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
CircularProgress
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Folder as ProjectIcon,
|
Folder as ProjectIcon,
|
||||||
FolderOpen as FolderOpenIcon,
|
FolderOpen as FolderOpenIcon,
|
||||||
@@ -21,7 +23,8 @@ import {
|
|||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
Delete as DeleteIcon,
|
Delete as DeleteIcon,
|
||||||
Tune as TuningIcon,
|
Tune as TuningIcon,
|
||||||
OpenInNew as OpenIcon
|
OpenInNew as OpenIcon,
|
||||||
|
Code as CodeIcon
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import ProjectResourceMonitor from './ProjectResourceMonitor'
|
import ProjectResourceMonitor from './ProjectResourceMonitor'
|
||||||
|
|
||||||
@@ -35,9 +38,13 @@ interface ProjectDetailsProps {
|
|||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onToggleStatus: () => void;
|
onToggleStatus: () => void;
|
||||||
onOpenSite: () => void;
|
onOpenSite: (useHost: boolean) => void;
|
||||||
onOpenFolder: () => void;
|
onOpenFolder: () => void;
|
||||||
|
onOpenVSCode: () => void;
|
||||||
onManageDb: () => void;
|
onManageDb: () => void;
|
||||||
|
onAddHost: (hostname: string) => void;
|
||||||
|
isTransitioning?: boolean;
|
||||||
|
setTransitioning?: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectDetails({
|
export default function ProjectDetails({
|
||||||
@@ -52,8 +59,20 @@ export default function ProjectDetails({
|
|||||||
onToggleStatus,
|
onToggleStatus,
|
||||||
onOpenSite,
|
onOpenSite,
|
||||||
onOpenFolder,
|
onOpenFolder,
|
||||||
onManageDb
|
onOpenVSCode,
|
||||||
|
onManageDb,
|
||||||
|
onAddHost,
|
||||||
|
isTransitioning = false,
|
||||||
|
setTransitioning
|
||||||
}: ProjectDetailsProps) {
|
}: ProjectDetailsProps) {
|
||||||
|
const [isHostMapped, setIsHostMapped] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (p?.host) {
|
||||||
|
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`;
|
||||||
|
window.api.invoke('hosts:check', finalHost).then(setIsHostMapped);
|
||||||
|
}
|
||||||
|
}, [p]);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0.3 }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0.3 }}>
|
||||||
@@ -70,12 +89,12 @@ export default function ProjectDetails({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(0,0,0,0.1)', height: '100%', overflow: 'hidden' }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(0,0,0,0.1)', height: '100%', overflow: 'hidden' }}>
|
||||||
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main', width: 48, height: 48, mt: 0.5 }}>
|
||||||
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main', width: 48, height: 48 }}>
|
|
||||||
<ProjectIcon />
|
<ProjectIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>{p.name}</Typography>
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>{p.name}</Typography>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'monospace' }}>
|
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'monospace' }}>
|
||||||
@@ -86,37 +105,98 @@ export default function ProjectDetails({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={isRunning ? <StopIcon /> : <StartIcon />}
|
disabled={isTransitioning}
|
||||||
|
startIcon={isTransitioning ? <CircularProgress size={16} /> : (isRunning ? <StopIcon /> : <StartIcon />)}
|
||||||
color={isRunning ? 'error' : 'primary'}
|
color={isRunning ? 'error' : 'primary'}
|
||||||
onClick={onToggleStatus}
|
onClick={async () => {
|
||||||
|
if (setTransitioning) setTransitioning(true);
|
||||||
|
try {
|
||||||
|
await onToggleStatus();
|
||||||
|
} finally {
|
||||||
|
if (setTransitioning) {
|
||||||
|
// Short delay to ensure status update is reflected
|
||||||
|
setTimeout(() => setTransitioning(false), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
height: 36,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
transition: 'all 0.1s ease-in-out',
|
||||||
|
'&:active': { transform: 'scale(0.96)' }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isRunning ? t('projects.stop_site') : t('projects.start_site')}
|
{isTransitioning ? t('common.processing') : (isRunning ? t('projects.stop_site') : t('projects.start_site'))}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<LaunchIcon />}
|
startIcon={<LaunchIcon />}
|
||||||
onClick={onOpenSite}
|
onClick={() => onOpenSite(isHostMapped)}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
height: 36,
|
||||||
|
transition: 'all 0.1s ease-in-out',
|
||||||
|
'&:active': { transform: 'scale(0.96)' }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('projects.open_site')}
|
{t('projects.open_site')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<CodeIcon />}
|
||||||
|
onClick={onOpenVSCode}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
height: 36,
|
||||||
|
borderColor: 'rgba(255,255,255,0.1)',
|
||||||
|
color: 'rgba(255,255,255,0.7)',
|
||||||
|
transition: 'all 0.1s ease-in-out',
|
||||||
|
'&:active': { transform: 'scale(0.96)' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
VS Code Aç
|
||||||
|
</Button>
|
||||||
|
{!isHostMapped && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
color="warning"
|
||||||
|
onClick={() => {
|
||||||
|
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`;
|
||||||
|
onAddHost(finalHost);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
height: 36,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
transition: 'all 0.1s ease-in-out',
|
||||||
|
'&:active': { transform: 'scale(0.96)' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hosts Dosyasına Ekle
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Tooltip title={t('common.edit')}>
|
<Tooltip title={t('common.edit')}>
|
||||||
<IconButton onClick={onEdit} sx={{ border: '1px solid rgba(255,255,255,0.1)' }}>
|
<IconButton onClick={onEdit} sx={{ width: 36, height: 36, border: '1px solid rgba(255,255,255,0.1)', borderRadius: '50%' }}>
|
||||||
<EditIcon fontSize="small" />
|
<EditIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('common.delete')}>
|
<Tooltip title={t('common.delete')}>
|
||||||
<IconButton onClick={onDelete} sx={{ border: '1px solid rgba(255,255,255,0.1)', '&:hover': { color: 'error.main', borderColor: 'error.main' } }}>
|
<IconButton onClick={onDelete} sx={{ width: 36, height: 36, border: '1px solid rgba(255,255,255,0.1)', borderRadius: '50%', '&:hover': { color: 'error.main', borderColor: 'error.main' } }}>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
|
||||||
<Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
|
<Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ interface ProjectManagerProps {
|
|||||||
onEditProject: (p: any) => void;
|
onEditProject: (p: any) => void;
|
||||||
onRemoveProject: (id: string) => void;
|
onRemoveProject: (id: string) => void;
|
||||||
onToggleStatus: (p: any) => void;
|
onToggleStatus: (p: any) => void;
|
||||||
onOpenSite: (p: any) => void;
|
onOpenSite: (p: any, useHost: boolean) => void;
|
||||||
onOpenFolder: (path: string) => void;
|
onOpenFolder: (path: string) => void;
|
||||||
|
onOpenVSCode: (path: string) => void;
|
||||||
|
onAddHost: (hostname: string) => void;
|
||||||
onManageDb: () => void;
|
onManageDb: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@ export default function ProjectManager({
|
|||||||
onToggleStatus,
|
onToggleStatus,
|
||||||
onOpenSite,
|
onOpenSite,
|
||||||
onOpenFolder,
|
onOpenFolder,
|
||||||
|
onOpenVSCode,
|
||||||
|
onAddHost,
|
||||||
onManageDb
|
onManageDb
|
||||||
}: ProjectManagerProps) {
|
}: ProjectManagerProps) {
|
||||||
|
|
||||||
@@ -74,8 +78,10 @@ export default function ProjectManager({
|
|||||||
onEdit={() => onEditProject(p)}
|
onEdit={() => onEditProject(p)}
|
||||||
onDelete={() => onRemoveProject(p.id)}
|
onDelete={() => onRemoveProject(p.id)}
|
||||||
onToggleStatus={() => onToggleStatus(p)}
|
onToggleStatus={() => onToggleStatus(p)}
|
||||||
onOpenSite={() => onOpenSite(p)}
|
onOpenSite={(useHost) => onOpenSite(p, useHost)}
|
||||||
onOpenFolder={() => onOpenFolder(p.path)}
|
onOpenFolder={() => onOpenFolder(p.path)}
|
||||||
|
onOpenVSCode={() => onOpenVSCode(p.path)}
|
||||||
|
onAddHost={onAddHost}
|
||||||
onManageDb={onManageDb}
|
onManageDb={onManageDb}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Box, Typography, LinearProgress } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
|
import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge'
|
||||||
|
|
||||||
interface ServiceInfo {
|
interface ServiceInfo {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -13,7 +14,7 @@ interface ProjectResourceMonitorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatMemory = (bytes?: number) => {
|
const formatMemory = (bytes?: number) => {
|
||||||
if (!bytes) return ''
|
if (!bytes) return '0 MB'
|
||||||
const mb = bytes / (1024 * 1024)
|
const mb = bytes / (1024 * 1024)
|
||||||
if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`
|
if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`
|
||||||
return `${Math.round(mb)} MB`
|
return `${Math.round(mb)} MB`
|
||||||
@@ -25,54 +26,96 @@ export default function ProjectResourceMonitor({ serverStats, phpStats, serverTy
|
|||||||
|
|
||||||
if (!hasServerStat && !hasPhpStat) return null;
|
if (!hasServerStat && !hasPhpStat) return null;
|
||||||
|
|
||||||
const renderMiniStats = (stats?: ServiceInfo, label: string = 'Service') => {
|
const renderServiceCard = (stats?: ServiceInfo, label: string = 'Service') => {
|
||||||
if (!stats) return null;
|
if (!stats) return null;
|
||||||
const cpu = stats.cpu || 0;
|
const cpu = Math.max(0, Math.min(100, Math.round(stats.cpu || 0)));
|
||||||
const memory = stats.memory || 0;
|
const memory = stats.memory || 0;
|
||||||
const ramMB = Math.round(memory / (1024 * 1024));
|
const ramMB = Math.round(memory / (1024 * 1024));
|
||||||
const ramPercent = Math.min((ramMB / 1024) * 100, 100);
|
const ramPercent = Math.min((ramMB / 1024) * 100, 100);
|
||||||
|
|
||||||
|
const getColor = (val: number) => {
|
||||||
|
if (val < 30) return '#33d9b2';
|
||||||
|
if (val < 70) return '#ffb142';
|
||||||
|
return '#ff5252';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
flex: 1,
|
||||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.65rem', fontWeight: 700 }}>{label.toUpperCase()}</Typography>
|
p: 2,
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
borderRadius: 3,
|
||||||
<Typography variant="caption" sx={{ color: 'primary.light', fontSize: '0.65rem', fontWeight: 700 }}>{cpu}%</Typography>
|
bgcolor: 'rgba(255,255,255,0.02)',
|
||||||
<Typography variant="caption" sx={{ color: 'secondary.light', fontSize: '0.65rem', fontWeight: 700 }}>{formatMemory(memory)}</Typography>
|
border: '1px solid rgba(255,255,255,0.05)',
|
||||||
</Box>
|
display: 'flex',
|
||||||
</Box>
|
flexDirection: 'column',
|
||||||
<Box sx={{ display: 'flex', gap: '2px' }}>
|
alignItems: 'center',
|
||||||
<LinearProgress
|
gap: 2
|
||||||
variant="determinate"
|
}}>
|
||||||
|
<Typography variant="caption" sx={{
|
||||||
|
color: 'rgba(255,255,255,0.6)',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 800,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.1em'
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 3, width: '100%', justifyContent: 'center' }}>
|
||||||
|
{/* CPU Gauge */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ width: 70, height: 70 }}>
|
||||||
|
<Gauge
|
||||||
value={cpu}
|
value={cpu}
|
||||||
sx={{
|
startAngle={-110}
|
||||||
height: 3,
|
endAngle={110}
|
||||||
borderRadius: 1,
|
innerRadius="75%"
|
||||||
flex: 1,
|
outerRadius="100%"
|
||||||
bgcolor: 'rgba(255,255,255,0.05)',
|
sx={() => ({
|
||||||
'.MuiLinearProgress-bar': { borderRadius: 1, backgroundImage: `linear-gradient(90deg, #33d9b2 ${cpu}%, #ff5252 100%)`, bgcolor: 'transparent' }
|
[`& .${gaugeClasses.valueText}`]: { fontSize: 14, fontWeight: 800, fill: getColor(cpu) },
|
||||||
}}
|
[`& .${gaugeClasses.valueArc}`]: { fill: getColor(cpu) },
|
||||||
|
[`& .${gaugeClasses.referenceArc}`]: { fill: 'rgba(255,255,255,0.05)' },
|
||||||
|
})}
|
||||||
|
text={({ value }) => `${value}%`}
|
||||||
/>
|
/>
|
||||||
<LinearProgress
|
</Box>
|
||||||
variant="determinate"
|
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.3)', fontSize: '0.6rem', mt: -1 }}>CPU</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* RAM Gauge */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ width: 70, height: 70 }}>
|
||||||
|
<Gauge
|
||||||
value={ramPercent}
|
value={ramPercent}
|
||||||
sx={{
|
startAngle={-110}
|
||||||
height: 3,
|
endAngle={110}
|
||||||
borderRadius: 1,
|
innerRadius="75%"
|
||||||
flex: 1,
|
outerRadius="100%"
|
||||||
bgcolor: 'rgba(255,255,255,0.05)',
|
sx={() => ({
|
||||||
'.MuiLinearProgress-bar': { borderRadius: 1, bgcolor: 'secondary.main' }
|
[`& .${gaugeClasses.valueText}`]: { fontSize: 12, fontWeight: 800, fill: 'secondary.light' },
|
||||||
}}
|
[`& .${gaugeClasses.valueArc}`]: { fill: 'secondary.main' },
|
||||||
|
[`& .${gaugeClasses.referenceArc}`]: { fill: 'rgba(255,255,255,0.05)' },
|
||||||
|
})}
|
||||||
|
text={() => formatMemory(memory)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.3)', fontSize: '0.6rem', mt: -1 }}>RAM</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 1.5, display: 'flex', gap: 2 }}>
|
<Box sx={{
|
||||||
{renderMiniStats(serverStats, serverType)}
|
mt: 2.5,
|
||||||
{renderMiniStats(phpStats, 'PHP')}
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
{renderServiceCard(serverStats, serverType)}
|
||||||
|
{renderServiceCard(phpStats, 'PHP')}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import {
|
import {
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
Folder as ProjectIcon
|
Add as AddIcon
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
interface ProjectSidebarProps {
|
interface ProjectSidebarProps {
|
||||||
@@ -66,7 +66,7 @@ export default function ProjectSidebar({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title={t('projects.add_new')}>
|
<Tooltip title={t('projects.add_new')}>
|
||||||
<IconButton size="small" color="primary" onClick={onAddProject} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)' }}>
|
<IconButton size="small" color="primary" onClick={onAddProject} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)' }}>
|
||||||
<ProjectIcon fontSize="small" />
|
<AddIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user