feat: implement project management system with IPC services and multi-language support

This commit is contained in:
Ümit Tunç
2026-04-02 20:51:07 +03:00
parent 4184d137e6
commit 69c4c0c780
8 changed files with 698 additions and 82 deletions
+347
View File
@@ -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", "service": "nginx",
"action": "start", "action": "start",
+88 -14
View File
@@ -17,6 +17,31 @@ 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' import { hostsService } from '../services/HostsService'
import net from 'net'
async function waitForPort(port: number, timeout: number = 5000): Promise<boolean> {
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 { function findExecutable(dir: string, name: string): string | null {
if (!fs.existsSync(dir)) return null if (!fs.existsSync(dir)) return null
@@ -433,7 +458,8 @@ export function registerIpcHandlers(): void {
const errorLogs = await processManager.getLastErrorLogs('nginx') const errorLogs = await processManager.getLastErrorLogs('nginx')
return { success: false, message: `Nginx başlatılamadı.\n\n${errorLogs.join('\n')}` } 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') { if (serviceName === 'apache') {
@@ -446,7 +472,8 @@ export function registerIpcHandlers(): void {
const errorLogs = await processManager.getLastErrorLogs('apache') const errorLogs = await processManager.getLastErrorLogs('apache')
return { success: false, message: `Apache başlatılamadı.\n\n${errorLogs.join('\n')}` } 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')) { 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 // Add a small delay to ensure previous instance (if any) is fully killed
await sleep(1000) 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) { if (!success) {
const errorLogs = await processManager.getLastErrorLogs(serviceName) const errorLogs = await processManager.getLastErrorLogs(serviceName)
return { success: false, message: `${phpVersion || ''} PHP başlatılamadı.\n\n${errorLogs.join('\n')}` } 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')) { if (serviceName.startsWith('mariadb') || serviceName.startsWith('mysql')) {
@@ -977,6 +1005,62 @@ export function registerIpcHandlers(): void {
return { success: true, settings } 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 // Nginx Config Management
ipcMain.handle('nginx:config:get', async () => { ipcMain.handle('nginx:config:get', async () => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf') const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
@@ -1321,14 +1405,4 @@ export function registerIpcHandlers(): void {
return '' 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 }
}
})
} }
+2 -2
View File
@@ -100,7 +100,7 @@ export class ProcessManager {
}) })
} }
async startService(name: string, binPath: string, args: string[], shell: boolean = true): Promise<boolean> { async startService(name: string, binPath: string, args: string[], shell: boolean = true, envVars: Record<string, string> = {}): Promise<boolean> {
if (this.processes.has(name)) { if (this.processes.has(name)) {
return true return true
} }
@@ -113,7 +113,7 @@ export class ProcessManager {
try { try {
this.addLog(name, `${t('common.starting')}... (${binPath})`) this.addLog(name, `${t('common.starting')}... (${binPath})`)
const serviceEnv = { ...process.env } const serviceEnv = { ...process.env, ...envVars }
const binDirPath = path.dirname(binPath) const binDirPath = path.dirname(binPath)
if (serviceEnv.PATH) { if (serviceEnv.PATH) {
serviceEnv.PATH = `${binDirPath}${path.delimiter}${serviceEnv.PATH}` serviceEnv.PATH = `${binDirPath}${path.delimiter}${serviceEnv.PATH}`
@@ -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<ProjectConfigEditorProps> = ({ value, onChange, serverType }) => {
const isApache = serverType === 'apache'
const language = isApache ? 'apacheconf' : 'nginx'
const prismLang = isApache ? Prism.languages.apacheconf : Prism.languages.nginx
return (
<div style={{
height: '100%',
overflow: 'auto',
borderRadius: '0px',
backgroundColor: '#1e1e1e',
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: '14px',
position: 'relative'
}}>
<Editor
value={value}
onValueChange={code => 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"
/>
<style>{`
.project-config-textarea {
outline: none !important;
caret-color: #fff !important;
white-space: pre !important;
overflow-wrap: normal !important;
}
.project-config-pre {
pointer-events: none;
white-space: pre !important;
overflow-wrap: normal !important;
}
.token.comment { color: #6a9955; }
.token.directive, .token.section { color: #569cd6; font-weight: bold; }
.token.variable { color: #9cdcfe; }
.token.string { color: #ce9178; }
.token.number { color: #b5cea8; }
.token.boolean { color: #569cd6; }
.token.operator, .token.punctuation { color: #d4d4d4; }
/* Apache specific */
.token.directive-inline { color: #569cd6; }
.token.directive-block { color: #569cd6; font-weight: bold; }
`}</style>
</div>
)
}
export default ProjectConfigEditor
@@ -11,7 +11,8 @@ import {
Divider, Divider,
Stack, Stack,
Tooltip, Tooltip,
CircularProgress CircularProgress,
Alert
} from '@mui/material' } from '@mui/material'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { import {
@@ -24,9 +25,12 @@ import {
Delete as DeleteIcon, Delete as DeleteIcon,
Tune as TuningIcon, Tune as TuningIcon,
OpenInNew as OpenIcon, OpenInNew as OpenIcon,
Code as CodeIcon Code as CodeIcon,
Save as SaveIcon
} from '@mui/icons-material' } from '@mui/icons-material'
import ProjectResourceMonitor from './ProjectResourceMonitor' import ProjectResourceMonitor from './ProjectResourceMonitor'
import ProjectConfigEditor from './ProjectConfigEditor'
import Swal from 'sweetalert2'
interface ProjectDetailsProps { interface ProjectDetailsProps {
project: any; project: any;
@@ -66,6 +70,9 @@ export default function ProjectDetails({
setTransitioning setTransitioning
}: ProjectDetailsProps) { }: ProjectDetailsProps) {
const [isHostMapped, setIsHostMapped] = useState<boolean>(true); const [isHostMapped, setIsHostMapped] = useState<boolean>(true);
const [configText, setConfigText] = useState<string>('');
const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false);
const [isConfigSaving, setIsConfigSaving] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (p?.host) { if (p?.host) {
@@ -73,6 +80,59 @@ export default function ProjectDetails({
window.api.invoke('hosts:check', finalHost).then(setIsHostMapped); window.api.invoke('hosts:check', finalHost).then(setIsHostMapped);
} }
}, [p]); }, [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) { 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 }}>
@@ -202,91 +262,147 @@ export default function ProjectDetails({
<Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary"> <Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={t('projects.overview')} /> <Tab label={t('projects.overview')} />
<Tab label={t('projects.database')} /> <Tab label={t('projects.database')} />
<Tab label={t('projects.server_settings')} />
</Tabs> </Tabs>
</Box> </Box>
<Box sx={{ p: 4, flexGrow: 1, overflowY: 'auto' }}> <Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{projectDetailTab === 0 && ( {projectDetailTab === 0 && (
<Grid container spacing={3}> <Box sx={{ p: 4 }}>
<Grid item xs={12} md={6}> <Grid container spacing={3}>
<Box sx={{ mb: 4 }}> <Grid item xs={12} md={6}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography> <Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}> <Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography>
<Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} /> <Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography>
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
</Box>
</Box> </Box>
</Box>
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography> <Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography> <Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography>
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} /> <Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
</Box>
</Box> </Box>
</Box>
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography> <Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography> <Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography>
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} /> <Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
</Box>
</Box> </Box>
</Box> </Grid>
<Grid item xs={12} md={6}>
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')}
</Typography>
<ProjectResourceMonitor
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
phpStats={status[`php:${p.phpVersion}`]}
/>
</Box>
</Grid>
</Grid> </Grid>
</Box>
<Grid item xs={12} md={6}>
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')}
</Typography>
<ProjectResourceMonitor
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
phpStats={status[`php:${p.phpVersion}`]}
/>
</Box>
</Grid>
</Grid>
)} )}
{projectDetailTab === 1 && ( {projectDetailTab === 1 && (
<Box sx={{ maxWidth: 600 }}> <Box sx={{ p: 4 }}>
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}> <Box sx={{ maxWidth: 600 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}> <Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Box> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography> <Box>
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography> <Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography>
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography>
</Box>
<Button
variant="outlined"
size="small"
startIcon={<OpenIcon />}
onClick={onManageDb}
>
{t('mariadb_manager.title')}
</Button>
</Box> </Box>
<Divider sx={{ mb: 3, opacity: 0.1 }} />
<Stack spacing={2}>
<Box>
<Typography variant="caption" color="text.secondary">Host</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Port</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Username</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Password</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography>
</Box>
</Stack>
</Box>
</Box>
</Box>
)}
{projectDetailTab === 2 && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CodeIcon color="primary" />
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{p.serverType.toUpperCase()} {t('projects.server_settings')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button
variant="outlined"
size="small" size="small"
startIcon={<OpenIcon />} startIcon={<TuningIcon />}
onClick={onManageDb} onClick={loadConfig}
sx={{ color: 'rgba(255,255,255,0.5)' }}
> >
{t('mariadb_manager.title')} {t('common.refresh')}
</Button>
<Button
variant="contained"
size="small"
color="primary"
startIcon={isConfigSaving ? <CircularProgress size={16} /> : <SaveIcon />}
disabled={isConfigSaving || isConfigLoading}
onClick={handleSaveConfig}
sx={{ px: 3 }}
>
{t('common.save').toUpperCase()}
</Button> </Button>
</Box> </Box>
</Box>
<Alert severity="info" sx={{ borderRadius: 0, py: 0.5, bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.light', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
{t('projects.manual_edit_warning')}
</Alert>
<Divider sx={{ mb: 3, opacity: 0.1 }} /> <Box sx={{ flexGrow: 1, position: 'relative', overflow: 'hidden' }}>
{isConfigLoading ? (
<Stack spacing={2}> <Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'rgba(0,0,0,0.5)', zIndex: 10 }}>
<Box> <CircularProgress />
<Typography variant="caption" color="text.secondary">Host</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography>
</Box> </Box>
<Box> ) : null}
<Typography variant="caption" color="text.secondary">Port</Typography> <ProjectConfigEditor
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography> value={configText}
</Box> onChange={setConfigText}
<Box> serverType={p.serverType}
<Typography variant="caption" color="text.secondary">Username</Typography> />
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Password</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography>
</Box>
</Stack>
</Box> </Box>
</Box> </Box>
)} )}
+2
View File
@@ -85,6 +85,8 @@
"open_site": "SITE ÖFFNEN", "open_site": "SITE ÖFFNEN",
"stop_site": "SITE STOPPEN", "stop_site": "SITE STOPPEN",
"start_site": "SITE STARTEN", "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." "no_project_selected": "Bitte wählen Sie ein Projekt aus der Liste aus oder erstellen Sie ein neues."
}, },
"settings": { "settings": {
+2
View File
@@ -91,6 +91,8 @@
"open_site": "OPEN SITE", "open_site": "OPEN SITE",
"stop_site": "STOP SITE", "stop_site": "STOP SITE",
"start_site": "START 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." "no_project_selected": "Please select a project from the list or create a new one."
}, },
"settings": { "settings": {
+3
View File
@@ -92,6 +92,9 @@
"open_site": "SİTEYİ AÇ", "open_site": "SİTEYİ AÇ",
"stop_site": "SİTEYİ DURDUR", "stop_site": "SİTEYİ DURDUR",
"start_site": "SİTEYİ BAŞLAT", "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." "no_project_selected": "Lütfen listeden bir proje seçin veya yeni bir tane oluşturun."
}, },
"settings": { "settings": {