feat: implement process management service and IPC bridge with initial renderer UI structure

This commit is contained in:
Ümit Tunç
2026-03-28 16:14:12 +03:00
parent 164fabc174
commit 8abffa741f
6 changed files with 417 additions and 39 deletions
+38 -4
View File
@@ -6,6 +6,7 @@ import { downloadService } from '../services/DownloadService'
import { projectService } from '../services/ProjectService'
import { phpManagerService } from '../services/PhpManagerService'
import { mySqlManagerService } from '../services/MySqlManagerService'
import { nginxManagerService } from '../services/NginxManagerService'
import path from 'path'
function findExecutable(dir: string, name: string): string | null {
@@ -49,14 +50,16 @@ export function registerIpcHandlers(): void {
}
if (serviceName === 'php') {
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {})
// Find any installed PHP version
const binPath = findExecutable(binDir, 'php-cgi.exe')
if (!binPath) return { success: false, message: 'PHP executable bulunamadı. Lütfen en az bir sürüm indirin.' }
const extDir = path.join(path.dirname(binPath), 'ext')
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {
EXT_DIR: extDir.replace(/\\/g, '\\\\') // PHP ini needs escaped backslashes or forward slashes
})
const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath])
return { success, message: success ? 'PHP başlatıldı.' : 'PHP dosyaları bulunamadı veya port meşgul.' }
return { success, message: success ? 'PHP başlatıldı.' : 'PHP devreye alınamadı (Hata loglarını kontrol edin).' }
}
if (serviceName === 'mysql') {
@@ -98,6 +101,10 @@ export function registerIpcHandlers(): void {
return processManager.getStatuses()
})
ipcMain.handle('services:logs', async (_event, serviceName: string) => {
return processManager.getLogs(serviceName)
})
// Binary management
ipcMain.handle('binaries:list-php', async () => {
return phpManagerService.getVersions()
@@ -153,6 +160,33 @@ export function registerIpcHandlers(): void {
}
})
// Nginx Versions
ipcMain.handle('nginx:versions', async () => {
return await nginxManagerService.getVersions()
})
ipcMain.handle('nginx:download', async (event, id: string) => {
const versions = await nginxManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz Nginx sürümü.' }
try {
nginxManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('nginx:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
nginxManagerService.updateProgress(id, p)
event.sender.send('nginx:progress', { id, progress: p })
})
nginxManagerService.updateProgress(id, 100, 'installed')
return { success: true, message: 'Nginx indirme tamamlandı.' }
} catch (error: any) {
nginxManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `Nginx hatası: ${error.message}` }
}
})
ipcMain.handle('dialog:select-directory', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory']
+86
View File
@@ -0,0 +1,86 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
export interface NginxVersion {
id: string
version: string
url: string
status: 'available' | 'downloading' | 'installed'
progress: number
}
export class NginxManagerService {
private versions: NginxVersion[] = []
private binDir: string
constructor() {
this.binDir = path.join(app.getPath('userData'), 'bin')
this.init()
}
private async init() {
if (!fs.existsSync(this.binDir)) {
fs.mkdirSync(this.binDir, { recursive: true })
}
}
async getVersions(): Promise<NginxVersion[]> {
// For Nginx, we can stick to one stable version or a small list
const stableVersion = '1.26.2'
const url = `https://nginx.org/download/nginx-${stableVersion}.zip`
const id = 'nginx'
const targetDir = path.join(this.binDir, id)
// Check if installed (look for nginx.exe anywhere in the zip extraction folder)
const isInstalled = this.isInstalled(targetDir)
const v: NginxVersion = {
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 findNginx = (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 (findNginx(p)) return true
} else if (f.toLowerCase() === 'nginx.exe') {
return true
}
}
return false
}
return findNginx(dir)
}
updateProgress(id: string, progress: number, status: NginxVersion['status'] = 'downloading') {
const v = this.versions.find(ev => ev.id === id)
if (v) {
v.progress = progress
v.status = status
}
}
}
export const nginxManagerService = new NginxManagerService()
+97 -21
View File
@@ -1,22 +1,80 @@
import { spawn, ChildProcess } from 'child_process'
import { join } from 'path'
import { spawn, ChildProcess, exec } from 'child_process'
import path from 'path'
import { app } from 'electron'
import fs from 'fs'
export class ProcessManager {
private processes: Map<string, ChildProcess> = new Map()
private logs: Map<string, string[]> = new Map()
private logDir: string
constructor() {
this.processes = new Map()
this.logDir = ''
}
private initLogs() {
if (this.logDir) return
try {
this.logDir = path.join(app.getPath('userData'), 'logs')
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true })
}
// Initialize with default logs
['nginx', 'php', 'mysql'].forEach(s => {
if (!this.logs.has(s)) {
this.logs.set(s, [`[${new Date().toLocaleTimeString()}] Sistem hazır. İşlem bekleniyor...`])
}
})
} catch (e) {
console.error('Failed to init logs:', e)
}
}
private addLog(name: string, data: string) {
this.initLogs()
if (!this.logs.has(name)) this.logs.set(name, [])
const serviceLogs = this.logs.get(name)!
const lines = data.split(/\r?\n/).filter(l => l.trim())
const timestamp = new Date().toLocaleTimeString()
lines.forEach(line => {
const formattedLine = `[${timestamp}] ${line}`
console.log(`[LOG:${name}] ${formattedLine}`)
serviceLogs.push(formattedLine)
// Persist to file
if (this.logDir) {
try {
fs.appendFileSync(path.join(this.logDir, `${name}.log`), formattedLine + '\n')
} catch (e) {
// Ignore write errors to file if it happens
}
}
})
// Keep last 100 lines in memory
if (serviceLogs.length > 100) {
this.logs.set(name, serviceLogs.slice(-100))
}
}
getLogs(name: string): string[] {
this.initLogs()
return this.logs.get(name) || []
}
async runOnce(binPath: string, args: string[]): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn(binPath, args, { stdio: 'inherit' })
const child = spawn(binPath, args, { stdio: 'pipe' })
child.stdout?.on('data', (data) => console.log(`[INIT] ${data}`))
child.stderr?.on('data', (data) => console.error(`[INIT ERROR] ${data}`))
child.on('close', (code) => {
resolve(code === 0)
})
child.on('error', () => {
child.on('error', (err) => {
console.error('RunOnce error:', err)
resolve(false)
})
})
@@ -24,43 +82,47 @@ export class ProcessManager {
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
if (this.processes.has(name)) {
console.log(`Service ${name} is already running.`)
return true
}
if (!fs.existsSync(binPath)) {
console.warn(`[DEV] Executable not found: ${binPath}. Mocking service ${name}.`)
const mockChild = {
kill: () => { },
on: () => { },
unref: () => { }
} as unknown as ChildProcess
this.processes.set(name, mockChild)
return true
this.addLog(name, `Executable not found: ${binPath}`)
return false
}
try {
this.addLog(name, `Starting service with: ${binPath} ${args.join(' ')}`)
const child = spawn(binPath, args, {
detached: true,
stdio: 'ignore'
shell: true,
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
cwd: path.dirname(binPath)
})
console.log(`[PROCESS] Started ${name} with PID ${child.pid}`)
this.addLog(name, `Process started (PID: ${child.pid})`)
child.stdout?.on('data', (data) => this.addLog(name, data.toString()))
child.stderr?.on('data', (data) => this.addLog(name, data.toString()))
child.on('error', (err) => {
console.error(`Failed to start service ${name}:`, err)
this.addLog(name, `Failed to start: ${err.message}`)
this.processes.delete(name)
})
child.on('exit', (code) => {
console.log(`Service ${name} exited with code ${code}`)
this.addLog(name, `Exited with code ${code}`)
this.processes.delete(name)
})
this.processes.set(name, child)
child.unref() // Allow the parent to exit independently
child.unref()
return true
} catch (error) {
console.error(`Error starting service ${name}:`, error)
} catch (error: any) {
this.addLog(name, `Spawn Error: ${error.message}`)
return false
}
}
@@ -68,11 +130,25 @@ export class ProcessManager {
async stopService(name: string): Promise<boolean> {
const child = this.processes.get(name)
if (child) {
this.addLog(name, 'Stopping service...')
child.kill()
this.processes.delete(name)
return true
}
return false
// Also force kill lingering processes
return this.forceKillAll(name)
}
async forceKillAll(name: string): Promise<boolean> {
return new Promise((resolve) => {
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'php' ? 'php-cgi.exe' : 'mysqld.exe'
this.addLog(name, `Force killing all ${executableName} processes...`)
exec(`taskkill /F /IM ${executableName} /T`, (_err) => {
// If it fails, usually it means the process isn't running, which is fine
resolve(true)
})
})
}
isServiceRunning(name: string): boolean {
+5 -1
View File
@@ -6,6 +6,7 @@ const api = {
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
getServiceLogs: (name: string) => electronAPI.ipcRenderer.invoke('services:logs'),
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
@@ -19,7 +20,10 @@ const api = {
onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)),
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'),
getSettings: () => electronAPI.ipcRenderer.invoke('config:get'),
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings)
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings),
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))
}
// Use `contextBridge` APIs to expose Electron APIs to
+190 -13
View File
@@ -53,7 +53,9 @@ import {
Refresh as RefreshIcon,
FolderOpen as FolderOpenIcon,
Save as SaveIcon,
Terminal as TerminalIcon
LanguageOutlined as WebIcon,
Terminal as TerminalIcon,
ContentCopy as CopyIcon
} from '@mui/icons-material'
import { useTranslation } from 'react-i18next'
@@ -90,6 +92,7 @@ function App(): JSX.Element {
const [projects, setProjects] = useState<any[]>([])
const [phpVersions, setPhpVersions] = useState<any[]>([])
const [mySqlVersions, setMySqlVersions] = useState<any[]>([])
const [nginxVersions, setNginxVersions] = useState<any[]>([])
const [loadingVersions, setLoadingVersions] = useState(false)
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
@@ -105,6 +108,11 @@ function App(): JSX.Element {
severity: 'success'
})
// Log States
const [logDialogOpen, setLogDialogOpen] = useState(false)
const [selectedService, setSelectedService] = useState('')
const [logs, setLogs] = useState<string[]>([])
const fetchStatus = async () => {
if (window.api && window.api.getServiceStatus) {
const currentStatus = await window.api.getServiceStatus()
@@ -140,10 +148,24 @@ function App(): JSX.Element {
}
}
const fetchNginxVersions = async () => {
if (window.api && window.api.getNginxVersions) {
const list = await window.api.getNginxVersions()
setNginxVersions(list || [])
}
}
const fetchLogs = async (service: string) => {
if (window.api && window.api.getServiceLogs) {
const list = await window.api.getServiceLogs(service)
setLogs(list || [])
}
}
const handleRefreshVersions = async () => {
setLoadingVersions(true)
try {
await Promise.all([fetchPhpVersions(), fetchMySqlVersions()])
await Promise.all([fetchPhpVersions(), fetchMySqlVersions(), fetchNginxVersions()])
} finally {
setLoadingVersions(false)
}
@@ -165,6 +187,7 @@ function App(): JSX.Element {
fetchProjects()
fetchPhpVersions()
fetchMySqlVersions()
fetchNginxVersions()
fetchSettings()
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
@@ -179,19 +202,35 @@ function App(): JSX.Element {
setMySqlVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
}) : null
const removeNginxListener = window.api.onNginxProgress ? window.api.onNginxProgress((data) => {
if (!data) return
const { id, progress } = data
setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
}) : null
const interval = setInterval(() => {
fetchStatus()
fetchProjects()
fetchPhpVersions()
fetchMySqlVersions()
fetchNginxVersions()
}, 5000)
let logInterval: NodeJS.Timeout | null = null
if (logDialogOpen && selectedService) {
logInterval = setInterval(() => {
fetchLogs(selectedService)
}, 1000)
}
return () => {
clearInterval(interval)
if (logInterval) clearInterval(logInterval)
if (typeof removePhpListener === 'function') removePhpListener()
if (typeof removeMySqlListener === 'function') removeMySqlListener()
if (typeof removeNginxListener === 'function') removeNginxListener()
}
}, [])
}, [logDialogOpen, selectedService])
const handleSelectPath = async () => {
if (window.api && window.api.selectDirectory) {
@@ -204,10 +243,19 @@ function App(): JSX.Element {
const handleToggleService = async (service: keyof ServiceStatus) => {
if (!window.api) return
const isStarting = status[service] === 'stopped'
if (isStarting) {
setStatus(prev => ({ ...prev, [service]: 'starting' }))
}
const result = status[service] === 'running'
? await window.api.stopService(service)
: await window.api.startService(service)
// Refresh status after operation
fetchStatus()
setNotification({
open: true,
message: result.message,
@@ -215,6 +263,12 @@ function App(): JSX.Element {
})
}
const handleOpenLogs = async (service: string) => {
setSelectedService(service)
setLogDialogOpen(true)
await fetchLogs(service)
}
const handleSaveSettings = async () => {
if (window.api && window.api.saveSettings) {
const result = await window.api.saveSettings(settings)
@@ -232,11 +286,17 @@ function App(): JSX.Element {
const handleStartAll = async () => {
if (!window.api) return
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
for (const s of services) {
if (status[s] === 'stopped') {
setStatus(prev => ({ ...prev, [s]: 'starting' }))
}
}
for (const s of services) {
if (status[s] !== 'running') {
await window.api.startService(s)
}
}
fetchStatus()
setNotification({ open: true, message: 'Tüm servisler başlatıldı.', severity: 'success' })
}
@@ -248,10 +308,14 @@ function App(): JSX.Element {
await window.api.stopService(s)
}
}
fetchStatus()
setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' })
}
const getStatusChip = (s: string) => {
if (s === 'starting') {
return <Chip label="BAŞLATILIYOR..." color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
}
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
}
@@ -294,6 +358,17 @@ function App(): JSX.Element {
fetchMySqlVersions()
}
const handleDownloadNginx = async (id: string) => {
if (!window.api) return
const result = await window.api.downloadNginxVersion(id)
setNotification({
open: true,
message: result.message,
severity: result.success ? 'success' : 'error'
})
fetchNginxVersions()
}
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
return (
@@ -359,7 +434,7 @@ function App(): JSX.Element {
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
Genel Ayarlar
</Typography>
{settingsTab < 2 && (
{settingsTab < 3 && (
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
Listeyi Yenile
</Button>
@@ -368,8 +443,9 @@ function App(): JSX.Element {
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
<Tab label="PHP Sürümleri" icon={<PhpIcon />} iconPosition="start" />
<Tab label="MySQL Sürümleri" icon={<DbIcon />} iconPosition="start" />
<Tab label="PHP" icon={<PhpIcon />} iconPosition="start" />
<Tab label="MySQL" icon={<DbIcon />} iconPosition="start" />
<Tab label="Nginx" icon={<WebIcon />} iconPosition="start" />
<Tab label="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
</Tabs>
</Box>
@@ -464,6 +540,44 @@ function App(): JSX.Element {
)}
{settingsTab === 2 && (
<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' }}>Nginx Web Sunucusu</Typography>
<Typography variant="body2" color="text.secondary">Sunucu için gerekli Nginx binary dosyalarını buradan indirebilirsiniz.</Typography>
</Box>
<List>
{nginxVersions.map((v) => (
<ListItem key={v.id} sx={{ py: 2 }}>
<ListItemIcon>
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
</ListItemIcon>
<ListItemText
primary={`Nginx ${v.version} (Stable)`}
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={() => handleDownloadNginx(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 === 3 && (
<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>
@@ -511,29 +625,50 @@ function App(): JSX.Element {
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
<List dense>
<ListItem sx={{ py: 1.5 }}>
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
<ListItemIcon><WebIcon color="primary" /></ListItemIcon>
<ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton size="small" onClick={() => handleOpenLogs('nginx')} title="Logları Görüntüle">
<TerminalIcon fontSize="small" />
</IconButton>
{getStatusChip(status.nginx)}
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
<Switch
checked={status.nginx === 'running' || status.nginx === 'starting'}
onChange={() => handleToggleService('nginx')}
disabled={status.nginx === 'starting'}
/>
</Box>
</ListItem>
<Divider variant="inset" />
<ListItem sx={{ py: 1.5 }}>
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
<ListItemText primary="PHP FastCGI" secondary={`Port: ${settings.phpPort}`} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton size="small" onClick={() => handleOpenLogs('php')} title="Logları Görüntüle">
<TerminalIcon fontSize="small" />
</IconButton>
{getStatusChip(status.php)}
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
<Switch
checked={status.php === 'running' || status.php === 'starting'}
onChange={() => handleToggleService('php')}
disabled={status.php === 'starting'}
/>
</Box>
</ListItem>
<Divider variant="inset" />
<ListItem sx={{ py: 1.5 }}>
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
<ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton size="small" onClick={() => handleOpenLogs('mysql')} title="Logları Görüntüle">
<TerminalIcon fontSize="small" />
</IconButton>
{getStatusChip(status.mysql)}
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />
<Switch
checked={status.mysql === 'running' || status.mysql === 'starting'}
onChange={() => handleToggleService('mysql')}
disabled={status.mysql === 'starting'}
/>
</Box>
</ListItem>
</List>
@@ -662,6 +797,48 @@ function App(): JSX.Element {
</Button>
</DialogActions>
</Dialog>
{/* Log Dialog */}
<Dialog open={logDialogOpen} onClose={() => setLogDialogOpen(false)} fullWidth maxWidth="md">
<DialogTitle sx={{ bgcolor: '#1e1e1e', color: '#fff', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TerminalIcon />
{selectedService.toUpperCase()} Konsol Çıktısı
</Box>
<Box>
<IconButton size="small" sx={{ color: '#fff' }} onClick={() => navigator.clipboard.writeText(logs.join('\n'))}>
<CopyIcon fontSize="small" />
</IconButton>
</Box>
</DialogTitle>
<DialogContent sx={{ bgcolor: '#000', p: 0 }}>
<Box
sx={{
p: 2,
fontFamily: 'Consolas, monospace',
fontSize: '0.85rem',
color: '#00ff00',
height: '400px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column'
}}
>
{logs.length === 0 ? (
<Typography variant="caption" sx={{ color: '#666' }}>Henüz log kaydı yok...</Typography>
) : (
logs.map((line, i) => (
<Box key={i} sx={{ mb: 0.5, borderLeft: '2px solid #333', pl: 1 }}>
{line}
</Box>
))
)}
</Box>
</DialogContent>
<DialogActions sx={{ bgcolor: '#1e1e1e' }}>
<Button onClick={() => setLogDialogOpen(false)} variant="contained" size="small">Kapat</Button>
</DialogActions>
</Dialog>
</Box>
)
}