feat: implement project management and service orchestration architecture

This commit is contained in:
Ümit Tunç
2026-03-30 22:16:55 +03:00
parent 46beccc484
commit e22b1872f0
10 changed files with 441 additions and 21 deletions
+10
View File
@@ -0,0 +1,10 @@
Alias /{{HOST}} "{{PATH}}"
<Directory "{{PATH}}">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:{{PHP_PORT}}"
</FilesMatch>
</Directory>
+60
View File
@@ -0,0 +1,60 @@
Define SRVROOT "{{SRVROOT}}"
Listen {{LISTEN_ADDRESS}}:{{PORT}}
LoadModule mpm_winnt_module modules/mod_mpm_winnt.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule dir_module modules/mod_dir.so
LoadModule mime_module modules/mod_mime.so
LoadModule alias_module modules/mod_alias.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
ServerAdmin admin@localhost
ServerName localhost:{{PORT}}
DocumentRoot "{{ROOT}}"
<Directory "{{ROOT}}">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog "{{LOG_DIR}}/apache_error.log"
LogLevel warn
<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog "{{LOG_DIR}}/apache_access.log" common
</IfModule>
<IfModule dir_module>
DirectoryIndex index.php index.html index.htm
</IfModule>
<Files ".ht*">
Require all denied
</Files>
# phpMyAdmin configuration
Alias /phpmyadmin "{{PHPMYADMIN_DIR}}"
<Directory "{{PHPMYADMIN_DIR}}">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:{{PHP_PORT}}"
</FilesMatch>
</Directory>
# Global PHP handler
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:{{PHP_PORT}}"
</FilesMatch>
# Project configurations
{{PROJECTS}}
+1 -1
View File
@@ -84,9 +84,9 @@ app.whenReady().then(() => {
optimizer.watchWindowShortcuts(window)
})
registerIpcHandlers()
createWindow()
createTray()
registerIpcHandlers()
// Generate CLI aliases on start
cliAliasService.generateAliases().catch(console.error)
+118 -6
View File
@@ -7,6 +7,7 @@ import { projectService } from '../services/ProjectService'
import { phpManagerService } from '../services/PhpManagerService'
import { mariaDbManagerService } from '../services/MariaDbManagerService'
import { nginxManagerService } from '../services/NginxManagerService'
import { apacheManagerService } from '../services/ApacheManagerService'
import { phpMyAdminService } from '../services/PhpMyAdminService'
import { portMappingService } from '../services/PortMappingService'
import path from 'path'
@@ -55,7 +56,8 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
}
// Generate project configs
const projects = await projectService.getProjects()
const allProjects = await projectService.getProjects()
const projects = allProjects.filter(p => !p.serverType || p.serverType === 'nginx')
for (const p of projects) {
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
@@ -142,12 +144,88 @@ async function reloadNginx(): Promise<{ success: boolean; message: string }> {
return { success: false, message: `Reload hatası: ${error.message}` }
}
}
async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ binPath: string; apacheDir: string; configPath: string }> {
const settings = configService.getSettings()
const binDir = path.join(configService.getBasePath(), 'bin')
const apacheDir = path.join(binDir, 'apache')
const isWindows = process.platform === 'win32';
const binPath = findExecutable(apacheDir, isWindows ? 'httpd.exe' : 'httpd')
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(app.getAppPath(), 'www')
// Ensure directories exist
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_apache')
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
// Generate project configs
const allProjects = await projectService.getProjects()
const projects = allProjects.filter(p => p.serverType === 'apache')
for (const p of projects) {
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
if (shouldGenerate) {
const phpPort = portMappingService.getPhpPort(p.phpVersion)
let normalizedPath = p.path.replace(/\\/g, '/')
if (!normalizedPath.endsWith('/')) normalizedPath += '/'
await configService.generateConfig('apache_project.conf.template', `projects_apache/${p.host}.conf`, {
HOST: p.host,
PATH: normalizedPath,
PHP_PORT: phpPort.toString()
})
}
}
const projectsInclusion = projects.length > 0
? `Include "${projectsConfDir.replace(/\\/g, '/')}/*.conf"`
: ''
const configPath = await configService.generateConfig('httpd.conf.template', 'httpd.conf', {
PORT: settings.apachePort || '8080',
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
PHP_PORT: settings.phpPort,
ROOT: rootPath.replace(/\\/g, '/'),
SRVROOT: apacheRoot.replace(/\\/g, '/'),
LOG_DIR: logDir.replace(/\\/g, '/'),
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/'),
PROJECTS: projectsInclusion
})
return { binPath, apacheDir, configPath }
}
async function restartApache(): Promise<{ success: boolean; message: string }> {
try {
const { binPath, configPath } = await rebuildApacheConfig(false)
await processManager.forceKillAll('apache')
await processManager.stopService('apache')
await new Promise(resolve => setTimeout(resolve, 1000))
const success = await processManager.startService('apache', binPath, ['-f', configPath], false)
if (success) {
return { success: true, message: 'Apache restart successful' }
} else {
const errorLogs = await processManager.getLastErrorLogs('apache')
return { success: false, message: `Apache restart failed:\n${errorLogs.join('\n')}` }
}
} catch (error: any) {
return { success: false, message: `Restart hatası: ${error.message}` }
}
}
export function registerIpcHandlers(): void {
// Service management
ipcMain.handle('services:reload', async (_event, serviceName: string) => {
if (serviceName !== 'nginx') return { success: false, message: 'Yalnızca Nginx için reload desteklenmektedir.' }
return await reloadNginx()
if (serviceName === 'nginx') return await reloadNginx()
if (serviceName === 'apache') return await restartApache() // Apache doesn't have a simple reload here
return { success: false, message: 'Bu servis için reload desteklenmiyor.' }
})
ipcMain.handle('services:start', async (_event, serviceName: string) => {
@@ -157,17 +235,24 @@ export function registerIpcHandlers(): void {
if (serviceName === 'nginx') {
const { binPath, nginxPrefix, configPath } = await rebuildNginxConfig(false)
const success = await processManager.startService('nginx', binPath, ['-p', nginxPrefix, '-c', configPath], false)
if (!success) {
const errorLogs = await processManager.getLastErrorLogs('nginx')
return { success: false, message: `Nginx başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx devreye alınamadı.' }
}
if (serviceName === 'apache') {
const { binPath, configPath } = await rebuildApacheConfig(false)
const success = await processManager.startService('apache', binPath, ['-f', configPath], false)
if (!success) {
const errorLogs = await processManager.getLastErrorLogs('apache')
return { success: false, message: `Apache başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? 'Apache başlatıldı.' : 'Apache devreye alınamadı.' }
}
if (serviceName.startsWith('php')) {
const versionMatch = serviceName.match(/^php:(.+)$/)
const phpVersion = versionMatch ? versionMatch[1] : null
@@ -451,6 +536,33 @@ export function registerIpcHandlers(): void {
}
})
// Apache Versions
ipcMain.handle('apache:versions', async () => {
return await apacheManagerService.getVersions()
})
ipcMain.handle('apache:download', async (event, id: string) => {
const versions = await apacheManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz Apache sürümü.' }
try {
apacheManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('apache:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
apacheManagerService.updateProgress(id, p)
event.sender.send('apache:progress', { id, progress: p })
})
apacheManagerService.updateProgress(id, 100, 'installed')
return { success: true, message: 'Apache indirme tamamlandı.' }
} catch (error: any) {
apacheManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `Apache hatası: ${error.message}` }
}
})
// Nginx Versions
ipcMain.handle('nginx:versions', async () => {
return await nginxManagerService.getVersions()
+86
View File
@@ -0,0 +1,86 @@
import fs from 'fs'
import path from 'path'
import { configService } from './ConfigService'
export interface ApacheVersion {
id: string
version: string
url: string
status: 'available' | 'downloading' | 'installed'
progress: number
}
export class ApacheManagerService {
private versions: ApacheVersion[] = []
private binDir: string
constructor() {
this.binDir = path.join(configService.getBasePath(), 'bin')
this.init()
}
private async init() {
if (!fs.existsSync(this.binDir)) {
fs.mkdirSync(this.binDir, { recursive: true })
}
}
async getVersions(): Promise<ApacheVersion[]> {
// Apache Lounge VS17 Win64 version
const stableVersion = '2.4.66'
const url = `https://www.apachelounge.com/download/VS18/binaries/httpd-2.4.66-260223-Win64-VS18.zip`
const id = 'apache'
const targetDir = path.join(this.binDir, id)
// Check if installed (look for httpd.exe anywhere in the zip extraction folder)
const isInstalled = this.isInstalled(targetDir)
const v: ApacheVersion = {
id,
version: stableVersion,
url,
status: isInstalled ? 'installed' : 'available',
progress: isInstalled ? 100 : 0
}
// Maintain internal state for progress updates
const existing = this.versions.find(ev => ev.id === id)
if (existing && existing.status === 'downloading') {
return [existing]
}
this.versions = [v]
return this.versions
}
private isInstalled(dir: string): boolean {
if (!fs.existsSync(dir)) return false
const findHttpd = (d: string): boolean => {
const files = fs.readdirSync(d)
for (const f of files) {
const p = path.join(d, f)
const s = fs.statSync(p)
if (s.isDirectory()) {
if (findHttpd(p)) return true
} else if (f.toLowerCase() === 'httpd.exe') {
return true
}
}
return false
}
return findHttpd(dir)
}
updateProgress(id: string, progress: number, status: ApacheVersion['status'] = 'downloading') {
const v = this.versions.find(ev => ev.id === id)
if (v) {
v.progress = progress
v.status = status
}
}
}
export const apacheManagerService = new ApacheManagerService()
+2
View File
@@ -8,6 +8,8 @@ export class ConfigService {
private settingsPath: string
private defaultSettings = {
nginxPort: '80',
apachePort: '8080',
serverType: 'nginx' as 'nginx' | 'apache',
phpPort: '9001',
mariaDbPort: '3306',
mariaDbPorts: {} as Record<string, string>,
+6 -4
View File
@@ -22,7 +22,7 @@ export class ProcessManager {
fs.mkdirSync(this.logDir, { recursive: true })
}
// Initialize with default logs
['nginx', 'php', 'mariadb'].forEach(s => {
['nginx', 'apache', 'php', 'mariadb'].forEach(s => {
if (!this.logs.has(s)) {
this.logs.set(s, [`[${new Date().toLocaleTimeString()}] Sistem hazır. İşlem bekleniyor...`])
}
@@ -209,12 +209,12 @@ export class ProcessManager {
} else {
const isWindows = process.platform === 'win32';
if (isWindows) {
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'php' ? 'php-cgi.exe' : 'mariadbd.exe'
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'apache' ? 'httpd.exe' : name === 'php' ? 'php-cgi.exe' : 'mariadbd.exe'
spawn('taskkill', ['/F', '/IM', executableName, '/T'], { windowsHide: true })
.on('close', () => resolve(true))
.on('error', () => resolve(true))
} else {
const processName = name === 'nginx' ? 'nginx' : name === 'php' ? 'php-cgi' : 'mariadbd'
const processName = name === 'nginx' ? 'nginx' : name === 'apache' ? 'httpd' : name === 'php' ? 'php-cgi' : 'mariadbd'
spawn('pkill', ['-f', processName], { windowsHide: true })
.on('close', () => resolve(true))
.on('error', () => resolve(true))
@@ -227,7 +227,7 @@ export class ProcessManager {
this.processes.delete(name)
}
const procName = name === 'nginx' ? 'nginx' : name === 'php' ? 'php-fpm' : 'mariadbd'
const procName = name === 'nginx' ? 'nginx' : name === 'apache' ? 'httpd' : name === 'php' ? 'php-fpm' : 'mariadbd'
spawn('pkill', ['-9', '-f', procName])
.on('close', () => resolve(true))
.on('error', () => resolve(true))
@@ -287,6 +287,7 @@ export class ProcessManager {
}
// Ensure defaults are present if stopped
if (!statuses['nginx']) statuses['nginx'] = 'stopped'
if (!statuses['apache']) statuses['apache'] = 'stopped'
if (!statuses['mariadb']) statuses['mariadb'] = 'stopped'
return statuses
}
@@ -295,6 +296,7 @@ export class ProcessManager {
if (!this.logDir) return
const logPaths: Record<string, string> = {
nginx: path.join(this.logDir, 'nginx_error.log'),
apache: path.join(this.logDir, 'apache_error.log'),
mariadb: path.join(this.logDir, 'mariadb_error.log')
}
+1
View File
@@ -9,6 +9,7 @@ export interface Project {
phpVersion: string
mariaDbVersion: string
host: string
serverType?: 'nginx' | 'apache'
}
export class ProjectService {
+3
View File
@@ -25,6 +25,9 @@ const api = {
getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'),
downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id),
onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)),
getApacheVersions: () => electronAPI.ipcRenderer.invoke('apache:versions'),
downloadApacheVersion: (id: string) => electronAPI.ipcRenderer.invoke('apache:download', id),
onApacheProgress: (callback: any) => electronAPI.ipcRenderer.on('apache:progress', (_event, data) => callback(data)),
pmaStatus: () => electronAPI.ipcRenderer.invoke('pma:status'),
pmaSetup: (mariaDbPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mariaDbPort),
onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data)),
+154 -10
View File
@@ -91,6 +91,8 @@ interface ServiceStatus {
interface AppSettings {
nginxPort: string
apachePort: string
serverType: 'nginx' | 'apache'
phpPort: string
mariaDbPort: string
mariaDbPorts?: Record<string, string>
@@ -102,6 +104,7 @@ function App(): JSX.Element {
const { t, i18n } = useTranslation()
const [status, setStatus] = useState<ServiceStatus>({
nginx: 'stopped',
apache: 'stopped',
php: 'stopped',
mariadb: 'stopped'
})
@@ -112,11 +115,14 @@ function App(): JSX.Element {
const [phpVersions, setPhpVersions] = useState<any[]>([])
const [mariaDbVersions, setMariaDbVersions] = useState<any[]>([])
const [nginxVersions, setNginxVersions] = useState<any[]>([])
const [apacheVersions, setApacheVersions] = useState<any[]>([])
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
const slugify = (text: string) => text.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
const [newProject, setNewProject] = useState<any>({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost' })
const [newProject, setNewProject] = useState<any>({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost', serverType: 'nginx' })
const [settings, setSettings] = useState<AppSettings>({
nginxPort: '80',
apachePort: '8080',
serverType: 'nginx',
phpPort: '9001',
mariaDbPort: '3306',
allowRemoteAccess: false,
@@ -151,7 +157,7 @@ function App(): JSX.Element {
const filePath = match[1].replace(/\\/g, '/')
const line = parseInt(match[2])
const fileName = filePath.split('/').pop() || ''
const isProjectConfig = filePath.includes('/generated_configs/projects/')
const isProjectConfig = filePath.includes('/generated_configs/projects/') || filePath.includes('/generated_configs/projects_apache/')
const host = isProjectConfig ? fileName.replace('.conf', '') : null
return { filePath, line, host }
}
@@ -198,6 +204,12 @@ function App(): JSX.Element {
}
}
const fetchApacheVersions = async () => {
if (!window.api || !window.api.getApacheVersions) return
const v = await window.api.getApacheVersions()
setApacheVersions(v || [])
}
const fetchLogs = async (service: string) => {
if (window.api && window.api.getServiceLogs) {
const list = await window.api.getServiceLogs(service)
@@ -238,6 +250,7 @@ function App(): JSX.Element {
fetchPhpVersions()
fetchMariaDbVersions()
fetchNginxVersions()
fetchApacheVersions()
fetchSettings()
fetchPmaStatus()
@@ -259,6 +272,12 @@ function App(): JSX.Element {
setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
}) : null
const removeApacheListener = window.api.onApacheProgress ? window.api.onApacheProgress((data) => {
if (!data) return
const { id, progress } = data
setApacheVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
}) : null
const removePmaListener = window.api.onPmaProgress ? window.api.onPmaProgress((p: number) => {
setPmaProgress(p)
}) : null
@@ -284,6 +303,7 @@ function App(): JSX.Element {
if (typeof removePhpListener === 'function') removePhpListener()
if (typeof removeMariaDbListener === 'function') removeMariaDbListener()
if (typeof removeNginxListener === 'function') removeNginxListener()
if (typeof removeApacheListener === 'function') removeApacheListener()
if (typeof removePmaListener === 'function') removePmaListener()
}
}, [logDialogOpen, selectedService])
@@ -392,7 +412,7 @@ function App(): JSX.Element {
const handleStartAll = async () => {
if (!window.api) return
const criticalServices = ['nginx']
const criticalServices = [settings.serverType]
for (const s of criticalServices) {
if (status[s] === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' }))
}
@@ -419,7 +439,7 @@ function App(): JSX.Element {
const handleStopAll = async () => {
if (!window.api) return
const criticalServices = ['nginx']
const criticalServices = ['nginx', 'apache']
const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (status[s] === 'running' || status[s] === 'starting'))
const mariaDbToStop = Object.keys(status).filter(s => (s.startsWith('mariadb:') || s.startsWith('mysql:')) && (status[s] === 'running' || status[s] === 'starting'))
@@ -587,6 +607,17 @@ function App(): JSX.Element {
fetchNginxVersions()
}
const handleDownloadApache = async (id: string) => {
if (!window.api) return
const result = await window.api.downloadApacheVersion(id)
setNotification({
open: true,
message: result.message,
severity: result.success ? 'success' : 'error'
})
fetchApacheVersions()
}
const handleSetupPma = async () => {
if (!window.api) return
setPmaLoading(true)
@@ -605,7 +636,8 @@ function App(): JSX.Element {
}
const handleOpenPma = () => {
window.open(`http://localhost:${settings.nginxPort}/phpmyadmin`, '_blank')
const port = settings.serverType === 'apache' ? settings.apachePort : settings.nginxPort
window.open(`http://localhost:${port}/phpmyadmin`, '_blank')
}
const handleOpenPhpExtensions = (version: string) => {
@@ -712,6 +744,7 @@ function App(): JSX.Element {
<Tab label="PHP" icon={<PhpIcon />} iconPosition="start" />
<Tab label="MariaDB" icon={<DbIcon />} iconPosition="start" />
<Tab label="Nginx" icon={<WebIcon />} iconPosition="start" />
<Tab label="Apache" icon={<ServerIcon />} iconPosition="start" />
<Tab label="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
</Tabs>
</Box>
@@ -865,10 +898,58 @@ function App(): JSX.Element {
)}
{settingsTab === 3 && (
<Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Apache Web Sunucusu</Typography>
<Typography variant="body2" color="text.secondary">Apache HTTP Server binary dosyalarını buradan yönetebilirsiniz.</Typography>
</Box>
<List>
{apacheVersions.map((v) => (
<ListItem key={v.id} sx={{ py: 2 }}>
<ListItemIcon>
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
</ListItemIcon>
<ListItemText
primary={`Apache ${v.version}`}
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
sx={{ flexGrow: 1 }}
/>
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
{v.status === 'available' && (
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadApache(v.id)}>
İNDİR
</Button>
)}
{v.status === 'downloading' && (
<Box sx={{ width: '100%', mt: 1 }}>
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
</Box>
)}
{v.status === 'installed' && (
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
)}
</Box>
</ListItem>
))}
</List>
</Paper>
)}
{settingsTab === 4 && (
<Box component={Stack} spacing={3}>
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Typography variant="h6" gutterBottom>Port Ayarları</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3, mt: 2 }}>
<FormControl variant="filled" size="small" fullWidth>
<InputLabel>Varsayılan Web Sunucusu</InputLabel>
<Select
value={settings.serverType || 'nginx'}
onChange={(e) => setSettings({ ...settings, serverType: e.target.value as any })}
>
<MenuItem value="nginx">Nginx (Performans)</MenuItem>
<MenuItem value="apache">Apache (htaccess Desteği)</MenuItem>
</Select>
</FormControl>
<TextField
label="Nginx Portu"
value={settings.nginxPort}
@@ -877,6 +958,14 @@ function App(): JSX.Element {
size="small"
helperText="Varsayılan: 80"
/>
<TextField
label="Apache Portu"
value={settings.apachePort}
onChange={(e) => setSettings({ ...settings, apachePort: e.target.value })}
variant="filled"
size="small"
helperText="Varsayılan: 8080"
/>
<TextField
label="PHP FastCGI Portu"
value={settings.phpPort}
@@ -931,6 +1020,7 @@ function App(): JSX.Element {
<Tabs value={dashboardTab} onChange={(_e, v) => setDashboardTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={t('common.all')} />
<Tab label="Nginx" />
<Tab label="Apache" />
<Tab label="PHP" />
<Tab label="MariaDB" />
</Tabs>
@@ -991,8 +1081,41 @@ function App(): JSX.Element {
</Grid>
)}
{/* Apache Box */}
{(dashboardTab === 0 || dashboardTab === 2) && (
<Grid item xs={12} sm={6} md={4}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<ServerIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status.apache)}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Apache</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {settings.apachePort}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton onClick={() => handleOpenLogs('apache')} title="Hata Logları" size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
</Box>
<Switch
checked={status.apache === 'running' || status.apache === 'starting'}
onChange={() => handleToggleService('apache')}
disabled={status.apache === 'starting'}
/>
</Box>
</Paper>
</Grid>
)}
{/* PHP Boxes */}
{(dashboardTab === 0 || dashboardTab === 2) && installedPhpVersions.map(v => (
{(dashboardTab === 0 || dashboardTab === 3) && installedPhpVersions.map(v => (
<Grid item xs={12} sm={6} md={4} key={v.id}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
@@ -1023,7 +1146,7 @@ function App(): JSX.Element {
))}
{/* MariaDB Boxes */}
{(dashboardTab === 0 || dashboardTab === 3) && mariaDbVersions.filter(v => v.status === 'installed').map(v => (
{(dashboardTab === 0 || dashboardTab === 4) && mariaDbVersions.filter(v => v.status === 'installed').map(v => (
<Grid item xs={12} sm={6} md={4} key={v.id}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
@@ -1255,6 +1378,12 @@ function App(): JSX.Element {
label={`PHP ${project.phpVersion}`}
sx={{ bgcolor: 'rgba(71, 74, 255, 0.1)', color: '#777bb4', fontWeight: 600, border: '1px solid rgba(71, 74, 255, 0.2)' }}
/>
<Chip
size="small"
icon={project.serverType === 'apache' ? <ServerIcon sx={{ fontSize: '1rem !important' }} /> : <WebIcon sx={{ fontSize: '1rem !important' }} />}
label={project.serverType === 'apache' ? 'Apache' : 'Nginx'}
sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)', color: '#fff', fontWeight: 600 }}
/>
</Box>
<Paper variant="outlined" sx={{
@@ -1275,7 +1404,10 @@ function App(): JSX.Element {
<Button
size="small"
endIcon={<LaunchIcon />}
onClick={() => window.open(`http://localhost:${settings.nginxPort}/${project.host}/`, '_blank')}
onClick={() => {
const port = project.serverType === 'apache' ? settings.apachePort : settings.nginxPort
window.open(`http://localhost:${port}/${project.host}/`, '_blank')
}}
sx={{
color: '#39A7FF',
textTransform: 'none',
@@ -1422,7 +1554,7 @@ function App(): JSX.Element {
<Dialog open={isProjectDialogOpen} onClose={() => {
setIsProjectDialogOpen(false)
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost' })
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost', serverType: 'nginx' })
}} fullWidth maxWidth="sm">
<DialogTitle>{newProject.id ? 'Proje Düzenle' : 'Yeni Proje Ekle'}</DialogTitle>
<DialogContent>
@@ -1471,12 +1603,24 @@ function App(): JSX.Element {
)}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel id="server-type-label">Web Sunucusu</InputLabel>
<Select
labelId="server-type-label"
value={newProject.serverType || 'nginx'}
label="Web Sunucusu"
onChange={(e) => setNewProject({ ...newProject, serverType: e.target.value as any })}
>
<MenuItem value="nginx">Nginx</MenuItem>
<MenuItem value="apache">Apache (htaccess desteği)</MenuItem>
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => {
setIsProjectDialogOpen(false)
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost' })
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost', serverType: 'nginx' })
}}>İptal</Button>
<Button
onClick={handleAddProject}