Skip to content

Commit e18f72d

Browse files
authored
feat: add infrastructure shutdown safeguards and enhance shutdown dialog text (#3858)
1 parent 2a39118 commit e18f72d

File tree

5 files changed

+156
-22
lines changed

5 files changed

+156
-22
lines changed

core/strings/src/commonMain/composeResources/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@
285285
<string name="resend">Resend</string>
286286
<string name="shutdown">Shutdown</string>
287287
<string name="cant_shutdown">Shutdown not supported on this device</string>
288+
<string name="shutdown_warning">⚠️ This will SHUTDOWN the node. Physical interaction will be required to turn it back on.</string>
289+
<string name="shutdown_critical_node">⚠️ This is a critical infrastructure node. Type the node name to confirm:</string>
290+
<string name="shutdown_node_name">Node: %1$s</string>
291+
<string name="shutdown_type_name">Type: %1$s</string>
288292
<string name="reboot">Reboot</string>
289293
<string name="traceroute">Traceroute</string>
290294
<string name="intro_show">Show Introduction</string>

feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ fun SettingsScreen(
244244
RadioConfigItemList(
245245
state = state,
246246
isManaged = localConfig.security.isManaged,
247+
node = viewModel.destNode.value,
247248
excludedModulesUnlocked = excludedModulesUnlocked,
248249
isDfuCapable = isDfuCapable,
249250
onPreserveFavoritesToggle = { viewModel.setPreserveFavorites(it) },

feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview
4949
import androidx.compose.ui.unit.dp
5050
import org.jetbrains.compose.resources.StringResource
5151
import org.jetbrains.compose.resources.stringResource
52+
import org.meshtastic.core.database.model.Node
5253
import org.meshtastic.core.navigation.FirmwareRoutes
5354
import org.meshtastic.core.navigation.Route
5455
import org.meshtastic.core.navigation.SettingsRoutes
@@ -76,6 +77,7 @@ import org.meshtastic.core.ui.theme.AppTheme
7677
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
7778
import org.meshtastic.feature.settings.navigation.ConfigRoute
7879
import org.meshtastic.feature.settings.navigation.ModuleRoute
80+
import org.meshtastic.feature.settings.radio.component.ShutdownConfirmationDialog
7981
import org.meshtastic.feature.settings.radio.component.WarningDialog
8082

8183
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -84,6 +86,7 @@ import org.meshtastic.feature.settings.radio.component.WarningDialog
8486
fun RadioConfigItemList(
8587
state: RadioConfigState,
8688
isManaged: Boolean,
89+
node: Node? = null,
8790
excludedModulesUnlocked: Boolean = false,
8891
isDfuCapable: Boolean = false,
8992
onPreserveFavoritesToggle: (Boolean) -> Unit = {},
@@ -158,28 +161,39 @@ fun RadioConfigItemList(
158161
AdminRoute.entries.forEach { route ->
159162
var showDialog by remember { mutableStateOf(false) }
160163
if (showDialog) {
161-
WarningDialog(
162-
title = "${stringResource(route.title)}?",
163-
text = {
164-
if (route == AdminRoute.NODEDB_RESET) {
165-
Row(
166-
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth(),
167-
verticalAlignment = Alignment.CenterVertically,
168-
horizontalArrangement = Arrangement.SpaceBetween,
169-
) {
170-
Text(text = stringResource(Res.string.preserve_favorites))
171-
Switch(
172-
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
173-
enabled = enabled,
174-
checked = state.nodeDbResetPreserveFavorites,
175-
onCheckedChange = onPreserveFavoritesToggle,
176-
)
164+
// Use enhanced confirmation for SHUTDOWN and REBOOT
165+
if (route == AdminRoute.SHUTDOWN || route == AdminRoute.REBOOT) {
166+
ShutdownConfirmationDialog(
167+
title = "${stringResource(route.title)}?",
168+
node = node,
169+
onDismiss = { showDialog = false },
170+
isShutdown = route == AdminRoute.SHUTDOWN,
171+
onConfirm = { onRouteClick(route) },
172+
)
173+
} else {
174+
WarningDialog(
175+
title = "${stringResource(route.title)}?",
176+
text = {
177+
if (route == AdminRoute.NODEDB_RESET) {
178+
Row(
179+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth(),
180+
verticalAlignment = Alignment.CenterVertically,
181+
horizontalArrangement = Arrangement.SpaceBetween,
182+
) {
183+
Text(text = stringResource(Res.string.preserve_favorites))
184+
Switch(
185+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
186+
enabled = enabled,
187+
checked = state.nodeDbResetPreserveFavorites,
188+
onCheckedChange = onPreserveFavoritesToggle,
189+
)
190+
}
177191
}
178-
}
179-
},
180-
onDismiss = { showDialog = false },
181-
onConfirm = { onRouteClick(route) },
182-
)
192+
},
193+
onDismiss = { showDialog = false },
194+
onConfirm = { onRouteClick(route) },
195+
)
196+
}
183197
}
184198

185199
ListItem(

feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
161161
val deviceConfig = state.radioConfig.device
162162
val formState = rememberConfigState(initialValue = deviceConfig)
163163
var selectedRole by rememberSaveable { mutableStateOf(formState.value.role) }
164-
val infrastructureRoles = listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.REPEATER)
164+
val infrastructureRoles =
165+
listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.ROUTER_LATE, DeviceConfig.Role.REPEATER)
165166
if (selectedRole != formState.value.role) {
166167
if (selectedRole in infrastructureRoles) {
167168
RouterRoleConfirmationDialog(
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright (c) 2025 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package org.meshtastic.feature.settings.radio.component
19+
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.Spacer
22+
import androidx.compose.foundation.layout.fillMaxWidth
23+
import androidx.compose.foundation.layout.height
24+
import androidx.compose.foundation.layout.padding
25+
import androidx.compose.material.icons.Icons
26+
import androidx.compose.material.icons.rounded.Warning
27+
import androidx.compose.material3.AlertDialog
28+
import androidx.compose.material3.Button
29+
import androidx.compose.material3.Icon
30+
import androidx.compose.material3.MaterialTheme
31+
import androidx.compose.material3.Text
32+
import androidx.compose.material3.TextButton
33+
import androidx.compose.runtime.Composable
34+
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.graphics.vector.ImageVector
36+
import androidx.compose.ui.text.font.FontWeight
37+
import androidx.compose.ui.text.style.TextAlign
38+
import androidx.compose.ui.tooling.preview.Preview
39+
import androidx.compose.ui.unit.dp
40+
import org.jetbrains.compose.resources.stringResource
41+
import org.meshtastic.core.database.model.Node
42+
import org.meshtastic.core.strings.Res
43+
import org.meshtastic.core.strings.cancel
44+
import org.meshtastic.core.strings.send
45+
import org.meshtastic.core.strings.shutdown_node_name
46+
import org.meshtastic.core.strings.shutdown_warning
47+
import org.meshtastic.core.ui.theme.AppTheme
48+
import org.meshtastic.proto.MeshProtos
49+
50+
@Composable
51+
fun ShutdownConfirmationDialog(
52+
title: String,
53+
node: Node?,
54+
onDismiss: () -> Unit,
55+
isShutdown: Boolean = true,
56+
icon: ImageVector? = Icons.Rounded.Warning,
57+
onConfirm: () -> Unit,
58+
) {
59+
val nodeLongName = node?.user?.longName ?: "Unknown Node"
60+
61+
AlertDialog(
62+
onDismissRequest = {},
63+
icon = { icon?.let { Icon(imageVector = it, contentDescription = null) } },
64+
title = { Text(text = title) },
65+
text = { ShutdownDialogContent(nodeLongName = nodeLongName, isShutdown = isShutdown) },
66+
dismissButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(Res.string.cancel)) } },
67+
confirmButton = {
68+
Button(
69+
onClick = {
70+
onDismiss()
71+
onConfirm()
72+
},
73+
) {
74+
Text(stringResource(Res.string.send))
75+
}
76+
},
77+
)
78+
}
79+
80+
@Composable
81+
private fun ShutdownDialogContent(nodeLongName: String, isShutdown: Boolean) {
82+
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
83+
Text(
84+
text = stringResource(Res.string.shutdown_node_name, nodeLongName),
85+
style = MaterialTheme.typography.titleMedium,
86+
fontWeight = FontWeight.Bold,
87+
modifier = Modifier.fillMaxWidth(),
88+
textAlign = TextAlign.Center,
89+
)
90+
91+
if (isShutdown) {
92+
Spacer(modifier = Modifier.height(8.dp))
93+
Text(
94+
text = stringResource(Res.string.shutdown_warning),
95+
style = MaterialTheme.typography.bodyMedium,
96+
color = MaterialTheme.colorScheme.error,
97+
modifier = Modifier.fillMaxWidth(),
98+
textAlign = TextAlign.Center,
99+
)
100+
}
101+
}
102+
}
103+
104+
@Preview
105+
@Composable
106+
private fun ShutdownConfirmationDialogPreview() {
107+
val mockNode =
108+
Node(
109+
num = 123,
110+
user = MeshProtos.User.newBuilder().setLongName("Rooftop Router Node").setShortName("ROOF").build(),
111+
)
112+
113+
AppTheme { ShutdownConfirmationDialog(title = "Shutdown?", node = mockNode, onDismiss = {}, onConfirm = {}) }
114+
}

0 commit comments

Comments
 (0)