diff --git a/assets/design-export/MANIFEST.md b/assets/design-export/MANIFEST.md index b7e0ccd34..71017b3b6 100644 --- a/assets/design-export/MANIFEST.md +++ b/assets/design-export/MANIFEST.md @@ -2874,3 +2874,6 @@ Nodes now follows the Backups toolbar: a plain search input, capped at 320px on All seven roots have JSON from Pencil `Get` and 2× PNG from `Export`. Toolbar, popover, desktop, fixed-cluster and mobile layouts were inspected using `TakeScreenshot`; the full screens retain their previous dimensions and content. JSON exports retain component references without elided children or editing placeholders. No React source, tests or lint suites were changed or run for this design update. +## Stop during provisioning + +The Pending server Overview (`o4LH8W`) shows Stop enabled in the header and in the lifecycle Quick actions card. Restart remains disabled until the server is running. diff --git a/assets/design-export/json/o4LH8W.json b/assets/design-export/json/o4LH8W.json index 2ff2adab1..63edd97dd 100644 --- a/assets/design-export/json/o4LH8W.json +++ b/assets/design-export/json/o4LH8W.json @@ -9,7 +9,7 @@ }, "clip": true, "width": 1440, - "height": 1700, + "height": 1320, "fill": "$background/background", "children": [ { @@ -145,6 +145,12 @@ "fontWeight": "normal" } ] + }, + "EvWbj": { + "opacity": 0.5 + }, + "fXzP0": { + "opacity": 1 } } }, @@ -1203,6 +1209,7 @@ "width": "fill_container", "padding": 0, "gap": 0, + "layout": "vertical", "children": [ { "type": "frame", @@ -1231,546 +1238,116 @@ }, { "type": "frame", - "id": "rKcnK", - "name": "Content", + "id": "KByTv", + "name": "Pending server lifecycle actions", "width": "fill_container", "layout": "vertical", - "gap": 14, + "gap": 4, "padding": [ - 4, - 12, - 12, - 12 + 8, + 0 ], "children": [ { "type": "frame", - "id": "I5WLq", - "name": "World Group", + "id": "GS04u", + "name": "Restart", + "opacity": 0.5, "width": "fill_container", - "layout": "vertical", - "gap": 2, + "gap": 12, + "padding": [ + 8, + 24 + ], + "alignItems": "center", "children": [ { - "type": "frame", - "id": "QPvvz", - "name": "World Label", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 8, - 8, - 4, - 8 - ], - "children": [ - { - "type": "text", - "id": "y0bcsS", - "name": "Label", - "fill": "$muted", - "content": "WORLD", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "kZHSI", - "name": "Set time", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "alignItems": "center", - "children": [ - { - "type": "icon", - "id": "aCLkV", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "timer", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "text", - "id": "Jq2Wt", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Set time", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - } - ] - }, - { - "type": "frame", - "id": "RgMXe", - "name": "Set weather", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "alignItems": "center", - "children": [ - { - "type": "icon", - "id": "v0lfW", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "cloud", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "text", - "id": "i7msV", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Set weather", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - } - ] - }, - { - "type": "frame", - "id": "x1SZz", - "name": "Save world", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "YW55P", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "save", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "yxeyi", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "wepSn", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Save world", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "jLIA7", - "name": "Description", - "fill": "$muted", - "content": "Flush the world to disk now.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "icon", + "id": "LoFmn", + "name": "Restart icon", + "width": 16, + "height": 16, + "icon": "rotate-cw", + "library": "lucide", + "fill": "$muted" }, { - "type": "frame", - "id": "G74JEx", - "name": "Output Echo", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 4, - 0, - 0, - 0 - ], - "children": [ - { - "type": "frame", - "id": "u4okt", - "name": "Echo", - "width": "fill_container", - "fill": "$default/default", - "cornerRadius": 6, - "stroke": "$border/border", - "strokeWidth": 1, - "gap": 12, - "padding": [ - 8, - 12 - ], - "justifyContent": "space_between", - "alignItems": "center", - "children": [ - { - "type": "text", - "id": "yVyrx", - "name": "Output Text", - "fill": "$foreground/foreground", - "content": "Saved the game", - "fontFamily": "$typography/font-mono", - "fontSize": 12, - "fontWeight": "normal" - }, - { - "type": "text", - "id": "Ui2fR", - "name": "Dismiss", - "fill": "$muted", - "content": "dismiss", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "text", + "id": "RmYnw", + "name": "Restart label", + "fill": "$foreground/foreground", + "content": "Restart", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" } ] }, { "type": "frame", - "id": "PlIEJ", - "name": "Server Group", + "id": "r9Mzzj", + "name": "Stop", "width": "fill_container", - "layout": "vertical", - "gap": 2, + "gap": 12, + "padding": [ + 8, + 24 + ], + "alignItems": "center", "children": [ { - "type": "frame", - "id": "I5UvR", - "name": "Server Label", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 8, - 8, - 4, - 8 - ], - "children": [ - { - "type": "text", - "id": "EouCC", - "name": "Label", - "fill": "$muted", - "content": "SERVER", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "lXQs3", - "name": "Broadcast message", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "jbGbW", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "megaphone", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "pdiph", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "Mj6Ty", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Broadcast message", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "p8uLm", - "name": "Description", - "fill": "$muted", - "content": "Send a chat message to everyone on the server.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] - }, - { - "type": "frame", - "id": "ZwCFe", - "name": "Sent Confirmation", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 4, - 0, - 0, - 38 - ], - "children": [ - { - "type": "frame", - "id": "l29RYg", - "name": "Sent Chip", - "fill": "$success/soft", - "cornerRadius": 9999, - "gap": 6, - "padding": [ - 5, - 10 - ], - "alignItems": "center", - "children": [ - { - "type": "icon", - "id": "fCYJy", - "name": "Check", - "width": 13, - "height": 13, - "icon": "check", - "library": "lucide", - "fill": "$success/soft-foreground" - }, - { - "type": "text", - "id": "k9xIe", - "name": "Label", - "fill": "$success/soft-foreground", - "content": "Broadcast sent", - "fontFamily": "$typography/font-sans", - "fontSize": 12, - "fontWeight": "500" - } - ] - } - ] - }, - { - "type": "frame", - "id": "W5Eda", - "name": "Reload config", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "UF7Qn", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "refresh-cw", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "D92Gdl", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "IBV4L", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Reload config", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "HHDND", - "name": "Description", - "fill": "$muted", - "content": "Reapply server.properties without a restart.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "icon", + "id": "k0puI", + "name": "Stop icon", + "width": 16, + "height": 16, + "icon": "square", + "library": "lucide", + "fill": "$muted" }, { - "type": "frame", - "id": "qtBNL", - "name": "Announce restart", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "jEX6x", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "alarm-clock", - "library": "lucide", - "fill": "$danger/danger" - }, - { - "type": "frame", - "id": "y5kB1", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "F8gSy", - "name": "Title", - "fill": "$danger/danger", - "content": "Announce restart", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "O6315W", - "name": "Description", - "fill": "$muted", - "content": "Warn players, save, and restart the server.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "text", + "id": "SwewK", + "name": "Stop label", + "fill": "$foreground/foreground", + "content": "Stop", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" } ] }, { "type": "frame", - "id": "b2jcOU", - "name": "Players Group", + "id": "sHiTe", + "name": "Open console", "width": "fill_container", - "layout": "vertical", - "gap": 2, + "gap": 12, + "padding": [ + 8, + 24 + ], + "alignItems": "center", "children": [ { - "type": "frame", - "id": "JNdR6", - "name": "Players Label", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 8, - 8, - 4, - 8 - ], - "children": [ - { - "type": "text", - "id": "Wy7gE", - "name": "Label", - "fill": "$muted", - "content": "PLAYERS", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "600", - "letterSpacing": 1 - } - ] + "type": "icon", + "id": "Y2z57y", + "name": "Open console icon", + "width": 16, + "height": 16, + "icon": "terminal", + "library": "lucide", + "fill": "$muted" }, { - "type": "frame", - "id": "OaCMP", - "name": "Toggle PvP", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "OAW27", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "swords", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "X7SFDl", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "Z1Gdp8", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Toggle PvP", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "hxvCh", - "name": "Description", - "fill": "$muted", - "content": "Enable or disable player-versus-player combat.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "text", + "id": "BjuXX", + "name": "Open console label", + "fill": "$foreground/foreground", + "content": "Open console", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" } ] } diff --git a/assets/design-export/screenshots/o4LH8W.png b/assets/design-export/screenshots/o4LH8W.png index 85f22f701..9c0bc9fe2 100644 Binary files a/assets/design-export/screenshots/o4LH8W.png and b/assets/design-export/screenshots/o4LH8W.png differ diff --git a/changelog/unreleased/932-stop-during-startup.md b/changelog/unreleased/932-stop-during-startup.md new file mode 100644 index 000000000..01e8b9a2a --- /dev/null +++ b/changelog/unreleased/932-stop-during-startup.md @@ -0,0 +1,3 @@ +# Stop a provisioning server + +The server header and Overview now let operators stop a server while it is pending or starting. diff --git a/design.pen b/design.pen index 876a2bbb1..065ef70d8 100644 --- a/design.pen +++ b/design.pen @@ -1,5 +1,5 @@ { - "version": "2.21", + "version": "2.22", "children": [ { "type": "frame", @@ -50447,7 +50447,7 @@ }, "clip": true, "width": 1440, - "height": 1700, + "height": 1320, "fill": "$background/background", "children": [ { @@ -50583,6 +50583,12 @@ "fontWeight": "normal" } ] + }, + "EvWbj": { + "opacity": 0.5 + }, + "fXzP0": { + "opacity": 1 } } }, @@ -51641,6 +51647,7 @@ "width": "fill_container", "padding": 0, "gap": 0, + "layout": "vertical", "children": [ { "type": "frame", @@ -51669,546 +51676,116 @@ }, { "type": "frame", - "id": "rKcnK", - "name": "Content", + "id": "KByTv", + "name": "Pending server lifecycle actions", "width": "fill_container", "layout": "vertical", - "gap": 14, + "gap": 4, "padding": [ - 4, - 12, - 12, - 12 + 8, + 0 ], "children": [ { "type": "frame", - "id": "I5WLq", - "name": "World Group", + "id": "GS04u", + "name": "Restart", + "opacity": 0.5, "width": "fill_container", - "layout": "vertical", - "gap": 2, + "gap": 12, + "padding": [ + 8, + 24 + ], + "alignItems": "center", "children": [ { - "type": "frame", - "id": "QPvvz", - "name": "World Label", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 8, - 8, - 4, - 8 - ], - "children": [ - { - "type": "text", - "id": "y0bcsS", - "name": "Label", - "fill": "$muted", - "content": "WORLD", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "kZHSI", - "name": "Set time", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "alignItems": "center", - "children": [ - { - "type": "icon", - "id": "aCLkV", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "timer", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "text", - "id": "Jq2Wt", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Set time", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - } - ] - }, - { - "type": "frame", - "id": "RgMXe", - "name": "Set weather", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "alignItems": "center", - "children": [ - { - "type": "icon", - "id": "v0lfW", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "cloud", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "text", - "id": "i7msV", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Set weather", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - } - ] - }, - { - "type": "frame", - "id": "x1SZz", - "name": "Save world", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "YW55P", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "save", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "yxeyi", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "wepSn", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Save world", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "jLIA7", - "name": "Description", - "fill": "$muted", - "content": "Flush the world to disk now.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "icon", + "id": "LoFmn", + "name": "Restart icon", + "width": 16, + "height": 16, + "icon": "rotate-cw", + "library": "lucide", + "fill": "$muted" }, { - "type": "frame", - "id": "G74JEx", - "name": "Output Echo", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 4, - 0, - 0, - 0 - ], - "children": [ - { - "type": "frame", - "id": "u4okt", - "name": "Echo", - "width": "fill_container", - "fill": "$default/default", - "cornerRadius": 6, - "stroke": "$border/border", - "strokeWidth": 1, - "gap": 12, - "padding": [ - 8, - 12 - ], - "justifyContent": "space_between", - "alignItems": "center", - "children": [ - { - "type": "text", - "id": "yVyrx", - "name": "Output Text", - "fill": "$foreground/foreground", - "content": "Saved the game", - "fontFamily": "$typography/font-mono", - "fontSize": 12, - "fontWeight": "normal" - }, - { - "type": "text", - "id": "Ui2fR", - "name": "Dismiss", - "fill": "$muted", - "content": "dismiss", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "text", + "id": "RmYnw", + "name": "Restart label", + "fill": "$foreground/foreground", + "content": "Restart", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" } ] }, { "type": "frame", - "id": "PlIEJ", - "name": "Server Group", + "id": "r9Mzzj", + "name": "Stop", "width": "fill_container", - "layout": "vertical", - "gap": 2, + "gap": 12, + "padding": [ + 8, + 24 + ], + "alignItems": "center", "children": [ { - "type": "frame", - "id": "I5UvR", - "name": "Server Label", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 8, - 8, - 4, - 8 - ], - "children": [ - { - "type": "text", - "id": "EouCC", - "name": "Label", - "fill": "$muted", - "content": "SERVER", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "lXQs3", - "name": "Broadcast message", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "jbGbW", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "megaphone", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "pdiph", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "Mj6Ty", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Broadcast message", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "p8uLm", - "name": "Description", - "fill": "$muted", - "content": "Send a chat message to everyone on the server.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] - }, - { - "type": "frame", - "id": "ZwCFe", - "name": "Sent Confirmation", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 4, - 0, - 0, - 38 - ], - "children": [ - { - "type": "frame", - "id": "l29RYg", - "name": "Sent Chip", - "fill": "$success/soft", - "cornerRadius": 9999, - "gap": 6, - "padding": [ - 5, - 10 - ], - "alignItems": "center", - "children": [ - { - "type": "icon", - "id": "fCYJy", - "name": "Check", - "width": 13, - "height": 13, - "icon": "check", - "library": "lucide", - "fill": "$success/soft-foreground" - }, - { - "type": "text", - "id": "k9xIe", - "name": "Label", - "fill": "$success/soft-foreground", - "content": "Broadcast sent", - "fontFamily": "$typography/font-sans", - "fontSize": 12, - "fontWeight": "500" - } - ] - } - ] - }, - { - "type": "frame", - "id": "W5Eda", - "name": "Reload config", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "UF7Qn", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "refresh-cw", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "D92Gdl", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "IBV4L", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Reload config", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "HHDND", - "name": "Description", - "fill": "$muted", - "content": "Reapply server.properties without a restart.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "icon", + "id": "k0puI", + "name": "Stop icon", + "width": 16, + "height": 16, + "icon": "square", + "library": "lucide", + "fill": "$muted" }, { - "type": "frame", - "id": "qtBNL", - "name": "Announce restart", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "jEX6x", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "alarm-clock", - "library": "lucide", - "fill": "$danger/danger" - }, - { - "type": "frame", - "id": "y5kB1", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "F8gSy", - "name": "Title", - "fill": "$danger/danger", - "content": "Announce restart", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "O6315W", - "name": "Description", - "fill": "$muted", - "content": "Warn players, save, and restart the server.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "text", + "id": "SwewK", + "name": "Stop label", + "fill": "$foreground/foreground", + "content": "Stop", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" } ] }, { "type": "frame", - "id": "b2jcOU", - "name": "Players Group", + "id": "sHiTe", + "name": "Open console", "width": "fill_container", - "layout": "vertical", - "gap": 2, + "gap": 12, + "padding": [ + 8, + 24 + ], + "alignItems": "center", "children": [ { - "type": "frame", - "id": "JNdR6", - "name": "Players Label", - "width": "fill_container", - "layout": "vertical", - "padding": [ - 8, - 8, - 4, - 8 - ], - "children": [ - { - "type": "text", - "id": "Wy7gE", - "name": "Label", - "fill": "$muted", - "content": "PLAYERS", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "600", - "letterSpacing": 1 - } - ] + "type": "icon", + "id": "Y2z57y", + "name": "Open console icon", + "width": 16, + "height": 16, + "icon": "terminal", + "library": "lucide", + "fill": "$muted" }, { - "type": "frame", - "id": "OaCMP", - "name": "Toggle PvP", - "width": "fill_container", - "cornerRadius": 6, - "gap": 12, - "padding": 8, - "children": [ - { - "type": "icon", - "id": "OAW27", - "name": "Icon", - "width": 18, - "height": 18, - "icon": "swords", - "library": "lucide", - "fill": "$muted" - }, - { - "type": "frame", - "id": "X7SFDl", - "name": "Text", - "width": "fill_container", - "layout": "vertical", - "gap": 2, - "children": [ - { - "type": "text", - "id": "Z1Gdp8", - "name": "Title", - "fill": "$foreground/foreground", - "content": "Toggle PvP", - "fontFamily": "$typography/font-sans", - "fontSize": 14, - "fontWeight": "500" - }, - { - "type": "text", - "id": "hxvCh", - "name": "Description", - "fill": "$muted", - "content": "Enable or disable player-versus-player combat.", - "fontFamily": "$typography/font-sans", - "fontSize": 11, - "fontWeight": "normal" - } - ] - } - ] + "type": "text", + "id": "BjuXX", + "name": "Open console label", + "fill": "$foreground/foreground", + "content": "Open console", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" } ] } diff --git a/web/e2e/screenshots/slice2a.spec.ts b/web/e2e/screenshots/slice2a.spec.ts index 9f85525af..f1012d78d 100644 --- a/web/e2e/screenshots/slice2a.spec.ts +++ b/web/e2e/screenshots/slice2a.spec.ts @@ -130,6 +130,14 @@ test.describe("Slice 2a: Servers + core tabs (Desktop — 1440x900) @screenshots await expect(page.getByText(/waiting on storage/i)).toBeVisible({ timeout: 10_000 }); await expect(page.getByText(/PVCProvisioningFailed/)).toBeVisible(); await page.waitForTimeout(200); + const stopButtons = page.getByRole("button", { name: "Stop", exact: true }); + await expect(stopButtons).toHaveCount(2); + await expect(stopButtons.first()).toBeEnabled(); + await expect(stopButtons.last()).toBeEnabled(); + const restartButtons = page.getByRole("button", { name: "Restart", exact: true }); + await expect(restartButtons).toHaveCount(2); + await expect(restartButtons.first()).toBeDisabled(); + await expect(restartButtons.last()).toBeDisabled(); await capture(page, "o4LH8W"); }); diff --git a/web/specs.md b/web/specs.md index 6f8520031..3602750e9 100644 --- a/web/specs.md +++ b/web/specs.md @@ -246,6 +246,7 @@ The old Radix-based `ui/` components that shipped before this rebuild — `butto 2. **ServerDetail Page** (`web/src/routes/ServerDetail.tsx`, 289 lines) - Full server view with header showing phase chip, uptime, player count, and action buttons + - Both the header and Overview offer Stop during Pending and Starting to users with control access to that server. - Tabbed interface (HeroUI Tabs) routing to nine sub-views: Overview, Events, Console, Logs, Files, Mods, Modpacks, Players, Backups, Capture, Settings - Phase chip from ui/PhaseChip; game icon from ui/GameIcon - Lifecycle action buttons (start/stop/restart) gated by phase state diff --git a/web/src/components/server/ServerActionsCard.test.tsx b/web/src/components/server/ServerActionsCard.test.tsx index d6f63f8ba..f2f36630e 100644 --- a/web/src/components/server/ServerActionsCard.test.tsx +++ b/web/src/components/server/ServerActionsCard.test.tsx @@ -1000,6 +1000,17 @@ describe("ServerActionsCard", () => { expect(onOpenConsole).toHaveBeenCalled(); }); + it.each(["Pending", "Starting"] as const)("can stop a provisioning server while %s", async (phase) => { + routeFetch("operator", []); + const gs: GameServer = { metadata: { name: "s1" }, spec: { templateRef: { name: "minecraft-java" } }, status: { phase } }; + renderWithQuery(); + const stop = await screen.findByRole("button", { name: /^Stop$/i }); + expect(stop).toBeEnabled(); + expect(screen.getByRole("button", { name: /^Restart$/i })).toBeDisabled(); + fireEvent.click(stop); + await waitFor(() => expect(fetchMock.mock.calls.filter(([url]) => String(url).includes("/servers/s1:stop?namespace=team-b"))).toHaveLength(1)); + }); + // F-126-adjacent: the generic lifecycle shortcuts' mutation had no error // handler — a failed Restart/Stop/Start/Wake left "Quick actions" silent. it("shows an error banner when a generic lifecycle shortcut fails", async () => { diff --git a/web/src/components/server/ServerActionsCard.tsx b/web/src/components/server/ServerActionsCard.tsx index 7a04be753..847c56109 100644 --- a/web/src/components/server/ServerActionsCard.tsx +++ b/web/src/components/server/ServerActionsCard.tsx @@ -211,6 +211,7 @@ export function ServerActionsCard({ // "not asleep yet" (players may be connected during the drain window). const asleep = gs.status?.idle?.asleep === true && phase !== "Stopping"; const running = phase === "Running"; + const canStop = running || asleep || phase === "Starting" || phase === "Pending"; const canStart = (phase === "Stopped" || phase === "Suspended" || phase === "Failed") && !asleep; return ( @@ -233,7 +234,7 @@ export function ServerActionsCard({ icon={Square} label={intl.formatMessage({ id: "web.components.server.server_actions_card.stop", defaultMessage: "Stop", description: "Server actions card: button that stops the running server; keep it short" })} onClick={() => lifecycle.mutate("stop")} - disabled={!canRun || (!running && !asleep) || lifecycle.isPending} + disabled={!canRun || !canStop || lifecycle.isPending} /> {asleep && ( { expect(screen.getByRole("button", { name: /^Restart$/i })).toBeDisabled(); }); - it("hides Start during a transitional phase (Starting)", async () => { + it.each(["Pending", "Starting"] as const)("can stop a remote server while %s", async (phase) => { + searchParams = { cluster: "remote", ns: "team-b" }; + const stopRequests: URL[] = []; server.use( - http.get("/servers/alpha", () => HttpResponse.json(makeServer({ status: { phase: "Starting" } }))), + http.get("/servers/alpha", () => HttpResponse.json(makeServer({ metadata: { namespace: "team-b" }, status: { phase } }))), + http.post(/\/servers\/alpha:stop$/, ({ request }) => { + stopRequests.push(new URL(request.url)); + return new HttpResponse(null, { status: 202 }); + }), ); renderWithQuery(); - await screen.findByRole("button", { name: /^Restart$/i }); + const stop = await screen.findByRole("button", { name: /^Stop$/i }); + await waitFor(() => expect(stop).toBeEnabled()); expect(screen.queryByRole("button", { name: /^Start$/i })).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: /^Stop$/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /^Restart$/i })).toBeDisabled(); + await userEvent.click(stop); + await waitFor(() => expect(stopRequests).toHaveLength(1)); + expect(stopRequests[0]?.searchParams.get("namespace")).toBe("team-b"); + expect(stopRequests[0]?.searchParams.get("cluster")).toBe("remote"); }); it("when asleep: Wake button shown, Start hidden", async () => { diff --git a/web/src/routes/ServerDetail.tsx b/web/src/routes/ServerDetail.tsx index 4ed7961fd..af6835c54 100644 --- a/web/src/routes/ServerDetail.tsx +++ b/web/src/routes/ServerDetail.tsx @@ -182,12 +182,7 @@ function ServerDetailView({ lookupTarget }: { lookupTarget: ResourceTarget }) { gs?.status?.conditions?.find((c) => c.type === "Ready"), gs?.status?.conditionArgs, ); - // Gate lifecycle actions on phase so they aren't fired during a - // transition (Starting/Stopping/Pending): Start only from a stopped - // state, Stop/Restart only while Running. act.isPending blocks - // duplicate mutations from a double-click. When asleep, Start is a silent - // no-op (spec.suspend is already false) — only :wake clears the operator's - // sleep marker. + // An idle server must wake through :wake to clear its sleep marker. const canStart = (phase === "Stopped" || phase === "Suspended" || phase === "Failed") && !asleep; const version = gs?.status?.agent?.gameVersion; const uptime = formatUptime(gs?.status?.startedAt, intl); @@ -293,7 +288,7 @@ function ServerDetailView({ lookupTarget }: { lookupTarget: ResourceTarget }) { variant="outline" className="rounded-full" onPress={() => act.mutate("stop")} - isDisabled={!access?.canControl || (!running && !asleep) || act.isPending} + isDisabled={!access?.canControl || (!running && !asleep && !provisioning) || act.isPending} > {intl.formatMessage({ id: "web.routes.server_detail.stop", defaultMessage: "Stop", description: "Server detail: header button that stops the running server, or puts a sleeping server to sleep fully; keep it short" })} {asleep && ( diff --git a/web/src/routes/Servers.test.tsx b/web/src/routes/Servers.test.tsx index c01085fa5..6965a7ffc 100644 --- a/web/src/routes/Servers.test.tsx +++ b/web/src/routes/Servers.test.tsx @@ -746,6 +746,15 @@ describe("ServersPage", () => { }, 10_000); }); + it.each(["Pending", "Starting"] as const)("allows stopping a %s server from its fleet row", async (phase) => { + server.use(http.get("/servers", () => HttpResponse.json({ + items: [makeServer({ metadata: { name: "booting", namespace: "gameplane-games" }, status: { phase } })], + }))); + renderWithQuery(); + const row = (await screen.findByText("booting")).closest("tr") as HTMLElement; + expect(within(row).getByTitle("Stop")).toBeEnabled(); + }); + // C1: an asleep server is phase Suspended, but :stop is still a real // action (it patches spec.suspend=true) — Stop must not be dead here. it("keeps Stop enabled for an asleep server", async () => { diff --git a/web/src/test/handlers.ts b/web/src/test/handlers.ts index 1186a0f23..dfb1db471 100644 --- a/web/src/test/handlers.ts +++ b/web/src/test/handlers.ts @@ -913,12 +913,20 @@ export function buildScreenshotHandlers() { items: data.templates, }), ), - http.get("/templates/:name", ({ params }) => { + http.get("/templates/:name", ({ params, cookies }) => { const name = String(params.name); const template = data.templates.find((t) => t.metadata.name === name); // Only the single-get carries a configSchema: Settings > Game // configuration reads it, the wizard's list view must stay unchanged. const configSchema = data.templateConfigSchemas[name]; + // This Pending server uses the lifecycle actions shown for templates + // without in-game commands. Other Minecraft examples keep their commands. + if (template && name === "minecraft-java" && cookies.e2e_server_variant === "pvc-failed") { + return HttpResponse.json({ + ...template, + spec: { ...template.spec, configSchema, capabilities: { ...template.spec.capabilities, actions: [] } }, + }); + } if (template && configSchema) { return HttpResponse.json({ ...template, spec: { ...template.spec, configSchema } }); }