diff --git a/service_events.json b/service_events.json index 8c8d5fa..b9f854f 100644 --- a/service_events.json +++ b/service_events.json @@ -1,4 +1,351 @@ [ + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "dddyqabhb", + "timestamp": 1775152237278 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "5clhuwgxn", + "timestamp": 1775152236025 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "3f3i7nrzx", + "timestamp": 1775151972287 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "cc098qksj", + "timestamp": 1775151972268 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "6814jwso7", + "timestamp": 1775151972248 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "e32wp6fdz", + "timestamp": 1775151972229 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "6ktzrdwkw", + "timestamp": 1775151971884 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "5dcg1ko1i", + "timestamp": 1775151971865 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "u8jthf8zj", + "timestamp": 1775151971244 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "xlp8uhm2c", + "timestamp": 1775151971178 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "ug698rcco", + "timestamp": 1775151837765 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "8lvo9sxz7", + "timestamp": 1775151836521 + }, + { + "service": "mariadb:11.4.2", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "a9vfbzxn2", + "timestamp": 1775151829018 + }, + { + "service": "mariadb:10.11.8", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "ze54njict", + "timestamp": 1775151829005 + }, + { + "service": "php:7.4.33", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "uaer37d6w", + "timestamp": 1775151828682 + }, + { + "service": "php:7.4.33", + "action": "stop", + "status": "info", + "id": "5or3hh6p5", + "timestamp": 1775151828671 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "yzmng3nm3", + "timestamp": 1775151828345 + }, + { + "service": "apache", + "action": "stop", + "status": "info", + "id": "5dj60xh68", + "timestamp": 1775151828339 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "taw24xi9n", + "timestamp": 1775151786087 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "d7cx0kco1", + "timestamp": 1775151786070 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "bren2nk3u", + "timestamp": 1775151786050 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "ubmufryso", + "timestamp": 1775151786033 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "bpbyftdye", + "timestamp": 1775151785673 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "cc6cp0j39", + "timestamp": 1775151785653 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "ta5nm4x9y", + "timestamp": 1775151785048 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "mtg8rhrxw", + "timestamp": 1775151784985 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "8n8vb3sst", + "timestamp": 1775151495788 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "u5vjd12e2", + "timestamp": 1775151494502 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "5wwt7m3au", + "timestamp": 1775151347535 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "hebcatt1d", + "timestamp": 1775151346107 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "njpd47glj", + "timestamp": 1775151316169 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "4n9uj134g", + "timestamp": 1775151314774 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "isd6s11sk", + "timestamp": 1775151306664 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "qlpij4nl8", + "timestamp": 1775151305292 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "nyn5eupfk", + "timestamp": 1775151292858 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "1p19qlpm9", + "timestamp": 1775151291558 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "ymbfiylvo", + "timestamp": 1775151242832 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "edk0n7ek6", + "timestamp": 1775151241487 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "6kkr2rr8p", + "timestamp": 1775151241115 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "vh49m05rr", + "timestamp": 1775151239716 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "n4nlkvjhx", + "timestamp": 1775151171177 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "0kg7gss85", + "timestamp": 1775151171159 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "xw56foupv", + "timestamp": 1775151171141 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "6599ey64j", + "timestamp": 1775151171123 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "b4fswrwcb", + "timestamp": 1775151170806 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "lhw2apj29", + "timestamp": 1775151170787 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "7fhj5rrnd", + "timestamp": 1775151170139 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "sde218qdo", + "timestamp": 1775151170080 + }, { "service": "nginx", "action": "start", diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index e908206..46fcdf5 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -17,6 +17,31 @@ import { cliAliasService } from '../services/CliAliasService' import { cliBinService } from '../services/CliBinService' import { serviceEventLogger } from '../services/ServiceEventLogger' import { hostsService } from '../services/HostsService' +import net from 'net' + +async function waitForPort(port: number, timeout: number = 5000): Promise { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + await new Promise((resolve, reject) => { + const client = net.createConnection({ port, host: '127.0.0.1' }, () => { + client.end(); + resolve(true); + }); + client.on('error', reject); + client.setTimeout(500); + client.on('timeout', () => { + client.destroy(); + reject(new Error('timeout')); + }); + }); + return true; + } catch (e) { + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + return false; +} function findExecutable(dir: string, name: string): string | null { if (!fs.existsSync(dir)) return null @@ -433,7 +458,8 @@ export function registerIpcHandlers(): void { 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ı.' } + const ready = await waitForPort(settings.nginxPort, 5000); + return { success: ready, message: ready ? 'Nginx başlatıldı.' : 'Nginx başlatıldı ancak port cevabı alınamadı.' } } if (serviceName === 'apache') { @@ -446,7 +472,8 @@ export function registerIpcHandlers(): void { 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ı.' } + const ready = await waitForPort(parseInt(settings.apachePort || '8080'), 5000); + return { success: ready, message: ready ? 'Apache başlatıldı.' : 'Apache başlatıldı ancak port cevabı alınamadı.' } } if (serviceName.startsWith('php')) { @@ -521,14 +548,15 @@ export function registerIpcHandlers(): void { // Add a small delay to ensure previous instance (if any) is fully killed await sleep(1000) - const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false) + const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false, { PHPRC: path.dirname(configPath) }) if (!success) { const errorLogs = await processManager.getLastErrorLogs(serviceName) return { success: false, message: `${phpVersion || ''} PHP başlatılamadı.\n\n${errorLogs.join('\n')}` } } - return { success, message: success ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP devreye alınamadı.` } + const ready = await waitForPort(phpPort, 5000); + return { success: ready, message: ready ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP başlatıldı ancak port cevabı alınamadı.` } } if (serviceName.startsWith('mariadb') || serviceName.startsWith('mysql')) { @@ -977,6 +1005,62 @@ export function registerIpcHandlers(): void { return { success: true, settings } }) + ipcMain.handle('services:clear-events', async () => { + try { + serviceEventLogger.clear() + return { success: true } + } catch (error: any) { + console.error('Failed to clear events:', error) + return { success: false, message: error.message } + } + }) + + ipcMain.handle('project:get-config', async (_event, { host, serverType }) => { + const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects' + const baseDir = path.join(configService.getBasePath(), 'generated_configs', projectsDir) + + // Try both host.conf and host.local.conf + const paths = [ + path.join(baseDir, `${host}.conf`), + path.join(baseDir, `${host}.local.conf`) + ] + + for (const p of paths) { + if (fs.existsSync(p)) { + return { content: fs.readFileSync(p, 'utf8'), path: p } + } + } + + return { content: '', message: `Konfigürasyon dosyası bulunamadı. Aranan yollar: ${paths.join(', ')}` } + }) + + ipcMain.handle('project:save-config', async (_event, { host, serverType, content }) => { + const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects' + const baseDir = path.join(configService.getBasePath(), 'generated_configs', projectsDir) + + // Try to find existing file first (to overwrite the right one) + const paths = [ + path.join(baseDir, `${host}.conf`), + path.join(baseDir, `${host}.local.conf`) + ] + + let configPath = paths[0] + for (const p of paths) { + if (fs.existsSync(p)) { + configPath = p + break + } + } + + let finalContent = content + if (!finalContent.includes('# MANUAL_EDIT')) { + finalContent = `# MANUAL_EDIT\n${finalContent}` + } + + fs.writeFileSync(configPath, finalContent, 'utf8') + return { success: true, message: 'Konfigürasyon başarıyla kaydedildi.' } + }) + // Nginx Config Management ipcMain.handle('nginx:config:get', async () => { const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf') @@ -1321,14 +1405,4 @@ export function registerIpcHandlers(): void { return '' } }) - - ipcMain.handle('services:clear-events', async () => { - try { - serviceEventLogger.clear() - return { success: true } - } catch (error: any) { - console.error('Failed to clear events:', error) - return { success: false, message: error.message } - } - }) } diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index a00af6e..9d66b13 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -100,7 +100,7 @@ export class ProcessManager { }) } - async startService(name: string, binPath: string, args: string[], shell: boolean = true): Promise { + async startService(name: string, binPath: string, args: string[], shell: boolean = true, envVars: Record = {}): Promise { if (this.processes.has(name)) { return true } @@ -113,7 +113,7 @@ export class ProcessManager { try { this.addLog(name, `${t('common.starting')}... (${binPath})`) - const serviceEnv = { ...process.env } + const serviceEnv = { ...process.env, ...envVars } const binDirPath = path.dirname(binPath) if (serviceEnv.PATH) { serviceEnv.PATH = `${binDirPath}${path.delimiter}${serviceEnv.PATH}` diff --git a/src/renderer/src/components/ProjectManager/ProjectConfigEditor.tsx b/src/renderer/src/components/ProjectManager/ProjectConfigEditor.tsx new file mode 100644 index 0000000..a25dbf4 --- /dev/null +++ b/src/renderer/src/components/ProjectManager/ProjectConfigEditor.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import Editor from 'react-simple-code-editor' +import Prism from 'prismjs' +import 'prismjs/components/prism-nginx' +import 'prismjs/components/prism-apacheconf' +import 'prismjs/themes/prism-tomorrow.css' + +interface ProjectConfigEditorProps { + value: string + onChange: (value: string) => void + serverType: string +} + +const ProjectConfigEditor: React.FC = ({ value, onChange, serverType }) => { + const isApache = serverType === 'apache' + const language = isApache ? 'apacheconf' : 'nginx' + const prismLang = isApache ? Prism.languages.apacheconf : Prism.languages.nginx + + return ( +
+ onChange(code)} + highlight={code => Prism.highlight(code, prismLang, language)} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 14, + minHeight: '100%', + outline: 'none', + backgroundColor: 'transparent' + }} + textareaClassName="project-config-textarea" + preClassName="project-config-pre" + /> + +
+ ) +} + +export default ProjectConfigEditor diff --git a/src/renderer/src/components/ProjectManager/ProjectDetails.tsx b/src/renderer/src/components/ProjectManager/ProjectDetails.tsx index 8c8bc14..e498fe6 100644 --- a/src/renderer/src/components/ProjectManager/ProjectDetails.tsx +++ b/src/renderer/src/components/ProjectManager/ProjectDetails.tsx @@ -11,7 +11,8 @@ import { Divider, Stack, Tooltip, - CircularProgress + CircularProgress, + Alert } from '@mui/material' import { useState, useEffect } from 'react' import { @@ -24,9 +25,12 @@ import { Delete as DeleteIcon, Tune as TuningIcon, OpenInNew as OpenIcon, - Code as CodeIcon + Code as CodeIcon, + Save as SaveIcon } from '@mui/icons-material' import ProjectResourceMonitor from './ProjectResourceMonitor' +import ProjectConfigEditor from './ProjectConfigEditor' +import Swal from 'sweetalert2' interface ProjectDetailsProps { project: any; @@ -66,6 +70,9 @@ export default function ProjectDetails({ setTransitioning }: ProjectDetailsProps) { const [isHostMapped, setIsHostMapped] = useState(true); + const [configText, setConfigText] = useState(''); + const [isConfigLoading, setIsConfigLoading] = useState(false); + const [isConfigSaving, setIsConfigSaving] = useState(false); useEffect(() => { if (p?.host) { @@ -73,6 +80,59 @@ export default function ProjectDetails({ window.api.invoke('hosts:check', finalHost).then(setIsHostMapped); } }, [p]); + + useEffect(() => { + if (projectDetailTab === 2 && p) { + loadConfig(); + } + }, [projectDetailTab, p]); + + const loadConfig = async () => { + setIsConfigLoading(true); + try { + const res = await window.api.invoke('project:get-config', { host: p.host, serverType: p.serverType }); + setConfigText(res.content); + } catch (e) { + console.error('Failed to load config:', e); + } finally { + setIsConfigLoading(false); + } + }; + + const handleSaveConfig = async () => { + setIsConfigSaving(true); + try { + const res = await window.api.invoke('project:save-config', { + host: p.host, + serverType: p.serverType, + content: configText + }); + if (res.success) { + Swal.fire({ + icon: 'success', + title: t('common.success'), + text: t('common.saved_successfully'), + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000, + background: '#1e1e1e', + color: '#fff' + }); + } + } catch (e: any) { + Swal.fire({ + icon: 'error', + title: t('common.error'), + text: e.message, + background: '#1e1e1e', + color: '#fff' + }); + } finally { + setIsConfigSaving(false); + } + }; + if (!p) { return ( @@ -202,91 +262,147 @@ export default function ProjectDetails({ setProjectDetailTab(v)} textColor="primary" indicatorColor="primary"> + - + {projectDetailTab === 0 && ( - - - - {t('projects.site_domain')} - - {p.host}.local - + + + + + {t('projects.site_domain')} + + {p.host}.local + + - - - {t('projects.web_server')} - - {p.serverType.toUpperCase()} - + + {t('projects.web_server')} + + {p.serverType.toUpperCase()} + + - - - {t('projects.php_version')} - - {p.phpVersion} - + + {t('projects.php_version')} + + {p.phpVersion} + + - + + + + + + {t('dashboard.status')} + + + + - - - - - {t('dashboard.status')} - - - - - + )} {projectDetailTab === 1 && ( - - - - - {t('projects.mariadb_version')} - MariaDB {p.mariadbVersion} + + + + + + {t('projects.mariadb_version')} + MariaDB {p.mariadbVersion} + + + + + + + + Host + localhost + + + Port + {settings.mariaDbPort} + + + Username + root + + + Password + (no password) + + + + + + )} + + {projectDetailTab === 2 && ( + + + + + + {p.serverType.toUpperCase()} {t('projects.server_settings')} + + + + + + + + {t('projects.manual_edit_warning')} + - - - - - Host - localhost + + {isConfigLoading ? ( + + - - Port - {settings.mariaDbPort} - - - Username - root - - - Password - (no password) - - + ) : null} + )} diff --git a/src/renderer/src/locales/de.json b/src/renderer/src/locales/de.json index 49348a1..cbd2e24 100644 --- a/src/renderer/src/locales/de.json +++ b/src/renderer/src/locales/de.json @@ -85,6 +85,8 @@ "open_site": "SITE ÖFFNEN", "stop_site": "SITE STOPPEN", "start_site": "SITE STARTEN", + "server_settings": "SERVER-EINSTELLUNGEN", + "manual_edit_warning": "Wenn Sie diese Einstellungen manuell bearbeiten, wird oben # MANUAL_EDIT hinzugefügt und das System beendet automatische Aktualisierungen.", "no_project_selected": "Bitte wählen Sie ein Projekt aus der Liste aus oder erstellen Sie ein neues." }, "settings": { diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json index a495dd2..cb745df 100644 --- a/src/renderer/src/locales/en.json +++ b/src/renderer/src/locales/en.json @@ -91,6 +91,8 @@ "open_site": "OPEN SITE", "stop_site": "STOP SITE", "start_site": "START SITE", + "server_settings": "SERVER SETTINGS", + "manual_edit_warning": "When you manually edit these settings, # MANUAL_EDIT is added to the top and the system stops automatic updates.", "no_project_selected": "Please select a project from the list or create a new one." }, "settings": { diff --git a/src/renderer/src/locales/tr.json b/src/renderer/src/locales/tr.json index 3a4536b..5f70ac5 100644 --- a/src/renderer/src/locales/tr.json +++ b/src/renderer/src/locales/tr.json @@ -92,6 +92,9 @@ "open_site": "SİTEYİ AÇ", "stop_site": "SİTEYİ DURDUR", "start_site": "SİTEYİ BAŞLAT", + "no_site_selected": "Lütfen önce bir site seçin.", + "server_settings": "SUNUCU AYARLARI", + "manual_edit_warning": "Bu ayarları manuel olarak düzenlediğinizde, en tepeye # MANUAL_EDIT eklenir ve sistem otomatik güncellemeleri durdurur.", "no_project_selected": "Lütfen listeden bir proje seçin veya yeni bir tane oluşturun." }, "settings": {