v1.6.0 — fix service 500, search bar, modal redesign #30
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.devlab.app"
|
||||
minSdk = 26
|
||||
targetSdk = 37
|
||||
versionCode = 6
|
||||
versionName = "1.5.0"
|
||||
versionCode = 7
|
||||
versionName = "1.6.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -338,6 +338,15 @@ private fun ConsoleTab(state: ServerDetailUiState, viewModel: ServerDetailViewMo
|
||||
private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewModel) {
|
||||
var showServiceDialog by remember { mutableStateOf(false) }
|
||||
var selectedService by remember { mutableStateOf<ServiceInfo?>(null) }
|
||||
var serviceQuery by remember { mutableStateOf("") }
|
||||
|
||||
val filtered = remember(state.services, serviceQuery) {
|
||||
if (serviceQuery.isBlank()) state.services
|
||||
else state.services.filter {
|
||||
it.name.contains(serviceQuery, ignoreCase = true) ||
|
||||
it.description.contains(serviceQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -351,7 +360,17 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
|
||||
Text("Refresh")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = serviceQuery,
|
||||
onValueChange = { serviceQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Search services...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (state.serviceActionResult != null) {
|
||||
Card(
|
||||
@@ -387,14 +406,14 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
|
||||
}
|
||||
}
|
||||
}
|
||||
state.services.isEmpty() -> {
|
||||
filtered.isEmpty() -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("No services found", color = Grey500)
|
||||
Text(if (serviceQuery.isNotBlank()) "No services match your search" else "No services found", color = Grey500)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
state.services.forEach { service ->
|
||||
filtered.forEach { service ->
|
||||
val isActive = service.status == "active"
|
||||
val isRunning = state.serviceActionRunning == "${service.name}:start" ||
|
||||
state.serviceActionRunning == "${service.name}:stop" ||
|
||||
@@ -446,33 +465,82 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
|
||||
if (showServiceDialog && selectedService != null) {
|
||||
val service = selectedService!!
|
||||
val isActive = service.status == "active"
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showServiceDialog = false },
|
||||
title = { Text(service.name, fontWeight = FontWeight.SemiBold) },
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.MiscellaneousServices,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = service.name, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text("Status: ${if (isActive) "Active" else service.status}", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Choose an action:", style = MaterialTheme.typography.bodySmall, color = Grey500)
|
||||
Surface(
|
||||
color = (if (isActive) Green500 else Grey500).copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(androidx.compose.foundation.shape.CircleShape)
|
||||
.background(if (isActive) Green500 else Grey500)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = if (isActive) "Active" else service.status.replaceFirstChar { it.uppercase() },
|
||||
color = if (isActive) Green500 else Grey500,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Text(
|
||||
text = "Actions",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Grey500
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (isActive) {
|
||||
ServiceActionChip("Restart", Icons.Default.Refresh, Orange500) {
|
||||
viewModel.serviceAction(service.name, "restart")
|
||||
showServiceDialog = false
|
||||
}
|
||||
ServiceActionChip("Stop", Icons.Default.Stop, Red500) {
|
||||
viewModel.serviceAction(service.name, "stop")
|
||||
showServiceDialog = false
|
||||
}
|
||||
ServiceActionChip("Reload", Icons.Default.Autorenew, Blue700) {
|
||||
viewModel.serviceAction(service.name, "reload")
|
||||
showServiceDialog = false
|
||||
}
|
||||
ServiceActionBtn(
|
||||
label = "Restart",
|
||||
icon = Icons.Default.Refresh,
|
||||
color = Orange500,
|
||||
onClick = { viewModel.serviceAction(service.name, "restart"); showServiceDialog = false }
|
||||
)
|
||||
ServiceActionBtn(
|
||||
label = "Stop",
|
||||
icon = Icons.Default.Stop,
|
||||
color = Red500,
|
||||
onClick = { viewModel.serviceAction(service.name, "stop"); showServiceDialog = false }
|
||||
)
|
||||
ServiceActionBtn(
|
||||
label = "Reload",
|
||||
icon = Icons.Default.Autorenew,
|
||||
color = Blue700,
|
||||
onClick = { viewModel.serviceAction(service.name, "reload"); showServiceDialog = false }
|
||||
)
|
||||
} else {
|
||||
ServiceActionChip("Start", Icons.Default.PlayArrow, Green500) {
|
||||
viewModel.serviceAction(service.name, "start")
|
||||
showServiceDialog = false
|
||||
}
|
||||
ServiceActionBtn(
|
||||
label = "Start",
|
||||
icon = Icons.Default.PlayArrow,
|
||||
color = Green500,
|
||||
onClick = { viewModel.serviceAction(service.name, "start"); showServiceDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -480,21 +548,26 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
|
||||
TextButton(onClick = { showServiceDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceActionChip(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
private fun ServiceActionBtn(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
|
||||
FilledTonalButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = color)
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = color.copy(alpha = 0.12f),
|
||||
contentColor = color
|
||||
)
|
||||
) {
|
||||
Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(label, fontWeight = FontWeight.Medium)
|
||||
Text(label, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -659,15 +659,18 @@ class ApiController
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
|
||||
$service = $input['service'] ?? '';
|
||||
$action = $input['action'] ?? '';
|
||||
$service = (string) ($input['service'] ?? '');
|
||||
$action = (string) ($input['action'] ?? '');
|
||||
$allowed = ['start', 'stop', 'restart', 'reload'];
|
||||
|
||||
if (empty($service) || !in_array($action, $allowed, true)) {
|
||||
if ($service === '' || !in_array($action, $allowed, true)) {
|
||||
$this->view->json(['error' => 'Invalid service or action'], 400);
|
||||
}
|
||||
|
||||
$sanitized = preg_replace('/[^a-zA-Z0-9\-_]/', '', $service);
|
||||
if ($sanitized === '') {
|
||||
$this->view->json(['error' => 'Invalid service name'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$ssh = new SSHService();
|
||||
@@ -678,7 +681,7 @@ class ApiController
|
||||
$result = $ssh->exec("sudo systemctl {$action} {$sanitized} 2>&1");
|
||||
$ssh->disconnect();
|
||||
|
||||
$exitCode = (int) ($result['exit_status'] ?? -1);
|
||||
$exitCode = isset($result['exit_status']) ? (int) $result['exit_status'] : -1;
|
||||
|
||||
$auditService = new AuditService();
|
||||
$auditService->logServerAction($id, "service_{$action}", "{$sanitized} {$action}");
|
||||
@@ -694,7 +697,7 @@ class ApiController
|
||||
} else {
|
||||
$this->view->json([
|
||||
'success' => false,
|
||||
'error' => "Failed to {$action} '{$sanitized}'.",
|
||||
'error' => "Failed to {$action} '{$sanitized}'. Exit code: {$exitCode}",
|
||||
'data' => ['output' => trim($result['output'] ?? '')],
|
||||
], 500);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user