feat: implement core architecture and dashboard UI for service and project management

This commit is contained in:
Ümit Tunç
2026-03-28 09:58:49 +03:00
parent 4195cdfa8b
commit 164fabc174
7 changed files with 192 additions and 25 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
[mysqld] [mysqld]
port=3306 port={{PORT}}
datadir="{{DATADIR}}" datadir="{{DATADIR}}"
socket="{{SOCKET}}" socket="{{SOCKET}}"
character-set-server=utf8mb4 character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci collation-server=utf8mb4_unicode_ci
[client] [client]
port=3306 port={{PORT}}
socket="{{SOCKET}}" socket="{{SOCKET}}"
default-character-set=utf8mb4 default-character-set=utf8mb4
+1 -1
View File
@@ -19,7 +19,7 @@ http {
} }
location ~ \.php$ { location ~ \.php$ {
fastcgi_pass 127.0.0.1:9001; fastcgi_pass 127.0.0.1:{{PHP_PORT}};
fastcgi_index index.php; fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params; include fastcgi_params;
+53 -11
View File
@@ -1,4 +1,5 @@
import { ipcMain, app, dialog } from 'electron' import { ipcMain, app, dialog } from 'electron'
import fs from 'fs'
import { processManager } from '../services/ProcessManager' import { processManager } from '../services/ProcessManager'
import { configService } from '../services/ConfigService' import { configService } from '../services/ConfigService'
import { downloadService } from '../services/DownloadService' import { downloadService } from '../services/DownloadService'
@@ -7,37 +8,78 @@ import { phpManagerService } from '../services/PhpManagerService'
import { mySqlManagerService } from '../services/MySqlManagerService' import { mySqlManagerService } from '../services/MySqlManagerService'
import path from 'path' import path from 'path'
function findExecutable(dir: string, name: string): string | null {
if (!fs.existsSync(dir)) return null
const files = fs.readdirSync(dir)
for (const file of files) {
const fullPath = path.join(dir, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
const found = findExecutable(fullPath, name)
if (found) return found
} else if (file.toLowerCase() === name.toLowerCase()) {
return fullPath
}
}
return null
}
export function registerIpcHandlers(): void { export function registerIpcHandlers(): void {
// Service management // Service management
ipcMain.handle('services:start', async (_event, serviceName: string) => { ipcMain.handle('services:start', async (_event, serviceName: string) => {
try { try {
const settings = configService.getSettings()
const binDir = path.join(app.getPath('userData'), 'bin')
if (serviceName === 'nginx') { if (serviceName === 'nginx') {
const rootPath = path.join(app.getAppPath(), 'www') const rootPath = path.join(app.getAppPath(), 'www')
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', { const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
PORT: '80', PORT: settings.nginxPort,
PHP_PORT: settings.phpPort,
ROOT: rootPath ROOT: rootPath
}) })
const binPath = downloadService.getBinPath('nginx', 'nginx.exe')
const nginxDir = path.join(binDir, 'nginx')
const binPath = findExecutable(nginxDir, 'nginx.exe')
if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' }
const success = await processManager.startService('nginx', binPath, ['-c', configPath]) const success = await processManager.startService('nginx', binPath, ['-c', configPath])
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx başlatılamadı.' } return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx dosyaları bulunamadı veya port meşgul.' }
} }
if (serviceName === 'php') { if (serviceName === 'php') {
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {}) const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {})
const binPath = downloadService.getBinPath('php8.2', 'php-cgi.exe') // Find any installed PHP version
const success = await processManager.startService('php', binPath, ['-b', '127.0.0.1:9001', '-c', configPath]) const binPath = findExecutable(binDir, 'php-cgi.exe')
return { success, message: success ? 'PHP başlatıldı.' : 'PHP başlatılamadı.' }
if (!binPath) return { success: false, message: 'PHP executable bulunamadı. Lütfen en az bir sürüm indirin.' }
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.' }
} }
if (serviceName === 'mysql') { if (serviceName === 'mysql') {
const dataDir = path.join(app.getPath('userData'), 'mysql_data') const dataDir = path.join(app.getPath('userData'), 'mysql_data')
const binPath = findExecutable(binDir, 'mysqld.exe')
if (!binPath) return { success: false, message: 'MySQL executable bulunamadı. Lütfen indirin.' }
// Initialize if data directory is empty
if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0) {
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
console.log('Initializing MySQL data directory...')
await processManager.runOnce(binPath, ['--initialize-insecure', `--datadir=${dataDir.replace(/\\/g, '/')}`])
}
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', { const configPath = await configService.generateConfig('my.ini.template', 'my.ini', {
PORT: settings.mysqlPort,
DATADIR: dataDir, DATADIR: dataDir,
SOCKET: '/tmp/mysql.sock' SOCKET: '/tmp/mysql.sock'
}) })
const binPath = downloadService.getBinPath('mysql', 'bin/mysqld.exe')
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath]) const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath])
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL başlatılamadı.' } return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL dosyaları bulunamadı veya port meşgul.' }
} }
return { success: false, message: 'Bilinmeyen servis.' } return { success: false, message: 'Bilinmeyen servis.' }
@@ -140,11 +182,11 @@ export function registerIpcHandlers(): void {
}) })
// Config management // Config management
ipcMain.handle('config:get', async (_event, key: string) => { ipcMain.handle('config:get', async () => {
return null return configService.getSettings()
}) })
ipcMain.handle('config:save', async (_event, config: any) => { ipcMain.handle('config:save', async (_event, config: any) => {
return { success: true } return { success: true, settings: configService.updateSettings(config) }
}) })
} }
+36
View File
@@ -5,14 +5,26 @@ import { app } from 'electron'
export class ConfigService { export class ConfigService {
private configDir: string private configDir: string
private templateDir: string private templateDir: string
private settingsPath: string
private defaultSettings = {
nginxPort: '80',
phpPort: '9001',
mysqlPort: '3306',
language: 'tr'
}
constructor() { constructor() {
this.templateDir = path.join(app.getAppPath(), 'config') this.templateDir = path.join(app.getAppPath(), 'config')
this.configDir = path.join(app.getPath('userData'), 'generated_configs') this.configDir = path.join(app.getPath('userData'), 'generated_configs')
this.settingsPath = path.join(app.getPath('userData'), 'settings.json')
if (!fs.existsSync(this.configDir)) { if (!fs.existsSync(this.configDir)) {
fs.mkdirSync(this.configDir, { recursive: true }) fs.mkdirSync(this.configDir, { recursive: true })
} }
if (!fs.existsSync(this.settingsPath)) {
fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2))
}
} }
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> { async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
@@ -37,6 +49,30 @@ export class ConfigService {
getGeneratedPath(targetName: string): string { getGeneratedPath(targetName: string): string {
return path.join(this.configDir, targetName) return path.join(this.configDir, targetName)
} }
getSettings(): any {
try {
if (fs.existsSync(this.settingsPath)) {
const data = fs.readFileSync(this.settingsPath, 'utf-8')
return { ...this.defaultSettings, ...JSON.parse(data) }
}
} catch (e) {
console.error('Failed to read settings:', e)
}
return this.defaultSettings
}
updateSettings(newSettings: any) {
try {
const current = this.getSettings()
const updated = { ...current, ...newSettings }
fs.writeFileSync(this.settingsPath, JSON.stringify(updated, null, 2))
return updated
} catch (e) {
console.error('Failed to save settings:', e)
throw e
}
}
} }
export const configService = new ConfigService() export const configService = new ConfigService()
+12
View File
@@ -10,6 +10,18 @@ export class ProcessManager {
this.processes = new Map() this.processes = new Map()
} }
async runOnce(binPath: string, args: string[]): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn(binPath, args, { stdio: 'inherit' })
child.on('close', (code) => {
resolve(code === 0)
})
child.on('error', () => {
resolve(false)
})
})
}
async startService(name: string, binPath: string, args: string[]): Promise<boolean> { async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
if (this.processes.has(name)) { if (this.processes.has(name)) {
console.log(`Service ${name} is already running.`) console.log(`Service ${name} is already running.`)
+3 -1
View File
@@ -17,7 +17,9 @@ const api = {
getMySqlVersions: () => electronAPI.ipcRenderer.invoke('mysql:versions'), getMySqlVersions: () => electronAPI.ipcRenderer.invoke('mysql:versions'),
downloadMySqlVersion: (id: string) => electronAPI.ipcRenderer.invoke('mysql:download', id), downloadMySqlVersion: (id: string) => electronAPI.ipcRenderer.invoke('mysql:download', id),
onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)), onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)),
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory') selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'),
getSettings: () => electronAPI.ipcRenderer.invoke('config:get'),
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings)
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to
+85 -10
View File
@@ -33,7 +33,8 @@ import {
Tabs, Tabs,
Tab, Tab,
InputAdornment, InputAdornment,
CircularProgress CircularProgress,
Stack
} from '@mui/material' } from '@mui/material'
import { import {
Dashboard as DashboardIcon, Dashboard as DashboardIcon,
@@ -50,7 +51,9 @@ import {
CheckCircle as InstalledIcon, CheckCircle as InstalledIcon,
CloudDownload as AvailableIcon, CloudDownload as AvailableIcon,
Refresh as RefreshIcon, Refresh as RefreshIcon,
FolderOpen as FolderOpenIcon FolderOpen as FolderOpenIcon,
Save as SaveIcon,
Terminal as TerminalIcon
} from '@mui/icons-material' } from '@mui/icons-material'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -68,6 +71,13 @@ interface ServiceStatus {
mysql: string mysql: string
} }
interface AppSettings {
nginxPort: string
phpPort: string
mysqlPort: string
language: string
}
function App(): JSX.Element { function App(): JSX.Element {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const [status, setStatus] = useState<ServiceStatus>({ const [status, setStatus] = useState<ServiceStatus>({
@@ -83,6 +93,12 @@ function App(): JSX.Element {
const [loadingVersions, setLoadingVersions] = useState(false) const [loadingVersions, setLoadingVersions] = useState(false)
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false) const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' }) const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
const [settings, setSettings] = useState<AppSettings>({
nginxPort: '80',
phpPort: '9001',
mysqlPort: '3306',
language: 'tr'
})
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({ const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
open: false, open: false,
message: '', message: '',
@@ -96,6 +112,13 @@ function App(): JSX.Element {
} }
} }
const fetchSettings = async () => {
if (window.api && window.api.getSettings) {
const s = await window.api.getSettings()
if (s) setSettings(s)
}
}
const fetchProjects = async () => { const fetchProjects = async () => {
if (window.api && window.api.getProjects) { if (window.api && window.api.getProjects) {
const list = await window.api.getProjects() const list = await window.api.getProjects()
@@ -142,6 +165,7 @@ function App(): JSX.Element {
fetchProjects() fetchProjects()
fetchPhpVersions() fetchPhpVersions()
fetchMySqlVersions() fetchMySqlVersions()
fetchSettings()
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
if (!data) return if (!data) return
@@ -160,7 +184,7 @@ function App(): JSX.Element {
fetchProjects() fetchProjects()
fetchPhpVersions() fetchPhpVersions()
fetchMySqlVersions() fetchMySqlVersions()
}, 3000) }, 5000)
return () => { return () => {
clearInterval(interval) clearInterval(interval)
@@ -191,6 +215,15 @@ function App(): JSX.Element {
}) })
} }
const handleSaveSettings = async () => {
if (window.api && window.api.saveSettings) {
const result = await window.api.saveSettings(settings)
if (result.success) {
setNotification({ open: true, message: 'Ayarlar kaydedildi.', severity: 'success' })
}
}
}
const toggleLanguage = () => { const toggleLanguage = () => {
const newLang = i18n.language === 'tr' ? 'en' : i18n.language === 'en' ? 'de' : 'tr' const newLang = i18n.language === 'tr' ? 'en' : i18n.language === 'en' ? 'de' : 'tr'
i18n.changeLanguage(newLang) i18n.changeLanguage(newLang)
@@ -326,15 +359,18 @@ function App(): JSX.Element {
<Typography variant="h5" sx={{ fontWeight: 'medium' }}> <Typography variant="h5" sx={{ fontWeight: 'medium' }}>
Genel Ayarlar Genel Ayarlar
</Typography> </Typography>
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}> {settingsTab < 2 && (
Listeyi Yenile <Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
</Button> Listeyi Yenile
</Button>
)}
</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"> <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="PHP Sürümleri" icon={<PhpIcon />} iconPosition="start" />
<Tab label="MySQL Sürümleri" icon={<DbIcon />} iconPosition="start" /> <Tab label="MySQL Sürümleri" icon={<DbIcon />} iconPosition="start" />
<Tab label="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
</Tabs> </Tabs>
</Box> </Box>
@@ -426,6 +462,45 @@ function App(): JSX.Element {
</List> </List>
</Paper> </Paper>
)} )}
{settingsTab === 2 && (
<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 }}>
<TextField
label="Nginx Portu"
value={settings.nginxPort}
onChange={(e) => setSettings({ ...settings, nginxPort: e.target.value })}
variant="filled"
size="small"
helperText="Varsayılan: 80"
/>
<TextField
label="PHP FastCGI Portu"
value={settings.phpPort}
onChange={(e) => setSettings({ ...settings, phpPort: e.target.value })}
variant="filled"
size="small"
helperText="Varsayılan: 9001"
/>
<TextField
label="MySQL Portu"
value={settings.mysqlPort}
onChange={(e) => setSettings({ ...settings, mysqlPort: e.target.value })}
variant="filled"
size="small"
helperText="Varsayılan: 3306"
/>
</Box>
</Paper>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button variant="contained" startIcon={<SaveIcon />} size="large" onClick={handleSaveSettings}>
Ayarları Kaydet
</Button>
</Box>
</Box>
)}
</Box> </Box>
)} )}
{activeTab === 'dashboard' && ( {activeTab === 'dashboard' && (
@@ -437,7 +512,7 @@ function App(): JSX.Element {
<List dense> <List dense>
<ListItem sx={{ py: 1.5 }}> <ListItem sx={{ py: 1.5 }}>
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon> <ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
<ListItemText primary="Nginx Web Sunucusu" secondary="Port: 80, 443" /> <ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{getStatusChip(status.nginx)} {getStatusChip(status.nginx)}
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} /> <Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
@@ -446,7 +521,7 @@ function App(): JSX.Element {
<Divider variant="inset" /> <Divider variant="inset" />
<ListItem sx={{ py: 1.5 }}> <ListItem sx={{ py: 1.5 }}>
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon> <ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
<ListItemText primary="PHP FastCGI" secondary="Sürüm: Seçilmedi" /> <ListItemText primary="PHP FastCGI" secondary={`Port: ${settings.phpPort}`} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{getStatusChip(status.php)} {getStatusChip(status.php)}
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} /> <Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
@@ -455,7 +530,7 @@ function App(): JSX.Element {
<Divider variant="inset" /> <Divider variant="inset" />
<ListItem sx={{ py: 1.5 }}> <ListItem sx={{ py: 1.5 }}>
<ListItemIcon><DbIcon color="primary" /></ListItemIcon> <ListItemIcon><DbIcon color="primary" /></ListItemIcon>
<ListItemText primary="MySQL / MariaDB" secondary="Port: 3306" /> <ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{getStatusChip(status.mysql)} {getStatusChip(status.mysql)}
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} /> <Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />