diff --git a/assets/design-export/MANIFEST.md b/assets/design-export/MANIFEST.md index 21d0c4060..f8656ad32 100644 --- a/assets/design-export/MANIFEST.md +++ b/assets/design-export/MANIFEST.md @@ -2797,3 +2797,16 @@ Source: `web/src/routes/tabs/settings/Resources.tsx` and the Configure step of ` - +163 (Rows 08–23): `a7o9C`, `akr4I`, `b4eaUf`, `Bbnga`, `BguST`, `Burtr`, `dBILX`, `dPP50`, `e9FnC`, `eGbsg`, `EZFW0`, `f1Vga`, `FtdkI`, `GayoL`, `hLB9Z`, `hlwx3`, `Hy9r0`, `IzuY2`, `j88bl0`, `KhYNc`, `kPmoo`, `lhKzN`, `m5kOm4`, `O08uaD`, `o4LH8W`, `OC804`, `oMuyw`, `P08Uw`, `pssCT`, `QFEg9`, `QQtUD`, `Ss0Yr`, `sSISK`, `SUtGZ`, `swxkJ`, `sZtDi`, `TE2jI`, `tY6RD`, `U9xSg`, `uCA23`, `UMJli`, `V1VhGE`, `VctzT`, `w1QYmf`, `wxINm`, `X405l`, `x7MJI`, `Xn5ns`, `xvlB6`, `YN8cE`, `ZU1BG` - +263 (Rows 24–57): `aMU7O`, `Be7ZW`, `Bq2Yg`, `bYDHC`, `C2LQE4`, `C9bR6`, `Cb3eC`, `cDiNb`, `D9QYZ`, `DBQyF`, `dhPtX`, `Dpb9f`, `DPrYX`, `dQV9N`, `dxdEi`, `DxKOh`, `e9lV4`, `EcoGD`, `EKJyM`, `epZO2`, `EWrn0`, `fbBqA`, `FBiMN`, `fK8Bi`, `FOVMf`, `g5mEpx`, `gWqMD`, `hmPL7`, `HVhmb`, `i1bLR`, `i8wib`, `IdbiB`, `iLm38`, `IMSD5`, `IyMFM`, `iYUWc`, `J5pjJ3`, `j9W8A`, `jXykV`, `K5XinZ`, `KaRFX`, `kK8Ji`, `l64bd`, `l7TRz9`, `lCTci`, `lWvcv`, `M2sA4u`, `n2qdi`, `n6Xlo`, `Ne5TA`, `nGJE1`, `NKiqf`, `nNGDX`, `O5kaV`, `oPF1n`, `p8QSPq`, `pnAbF`, `q31B6w`, `qFLfB`, `QgW58`, `QpEvu`, `r0heh`, `r139UP`, `RC3Kf`, `RodrS`, `RpHBg`, `RS0KZ`, `SUcad`, `T1wkiT`, `TBvTC`, `tr6cE`, `tSOeI`, `tTSdi`, `ugDSa`, `uMiwd`, `uoxQW`, `Uzzje`, `VfB0Y`, `Wj0V4`, `WrmU5`, `WZdnw`, `xCJlu`, `XR0f9`, `y3YcQ`, `Y5cmvI`, `yrxK2`, `Z0oyh`, `ZlLf0`, `zM0VF`, `zqzr4` + +## Incremental export 2026-10-31 — Wake on connect switch in server settings Lifecycle + +| Node | Name | Change | +| --- | --- | --- | +| `i1bLR` | Screen/Server Detail — Settings · Lifecycle | Re-exported (1440×1620, unchanged size; at y 46130 after the Row 26 move recorded for PR #815). New `Dz78Y` "Wake On Connect Section" in the Idle auto-sleep controls, between the wake windows and the caveat rows: an 11px "Wake on connect" label, a `Gameplane/Switch/Off` (`rh2QH`) instance with "Wake the server when a player connects", and the helper "While the server sleeps, a small pod holds its ports and wakes it when a player tries to join. Costs one extra pod per sleeping server. Games that only use UDP wake on the first attempt; the player joins on retry." | +| `A8vGwM` | Screen/Server Detail — Settings · Lifecycle (Wake on connect unavailable) | New copy of `i1bLR` to its right. The switch is dimmed (opacity 0.5), its label muted, and the helper reads "Not available for this game: its template marks every port as unable to wake the server." It shows a template whose advertised ports all set `wakeProtocol: none`, the case the operator's `wakeOnConnectEligible` never arms. | + +Merging master after PR #815 conflicted on `design.pen`. The file was taken from master and this change was re-applied with Pencil, which gave the new nodes new ids: the section was `R8V6LP` and the variant `paiCe` on the branch before the merge, so `json/paiCe.json` and `screenshots/paiCe.png` were replaced by the `A8vGwM` files. + +The operator has supported `spec.idle.wakeOnConnect` since v0.2.0-beta.8, but the dashboard had no control for it. No reusable component changed and Row 26 keeps its height, so no other node moved. `check-layout.js`: `NO ISSUES`. `pen-componentize` `audit.js`: nothing new on the two frames; the repeated "Field Auto-restart" and probe "row1" structures it lists predate this change. + +**Exports:** JSON from `Get(id, {depth: 80, includePathGeometry: true})`, pretty-printed (zero `"..."` elisions, `python3 -m json.tool` passes). PNG from `Export(ids, "png", …, {scale: 2})`: both 2880×3240. Headless `pen interactive` session (pen.dev CLI 0.3.14), with the lucide icon font served from the `lucide-static@0.563.0` npm tarball because unpkg.com is blocked in that sandbox. Body-text checks: `"Wake the server when a player connects"` only in `i1bLR.json` and `A8vGwM.json`; `"unable to wake the server"` only in `A8vGwM.json`. diff --git a/assets/design-export/json/A8vGwM.json b/assets/design-export/json/A8vGwM.json new file mode 100644 index 000000000..8e0b23059 --- /dev/null +++ b/assets/design-export/json/A8vGwM.json @@ -0,0 +1,1487 @@ +{ + "type": "frame", + "id": "A8vGwM", + "x": 1640, + "y": 46130, + "name": "Screen/Server Detail — Settings · Lifecycle (Wake on connect unavailable)", + "theme": { + "semantic": "dark" + }, + "clip": true, + "width": 1440, + "height": 1620, + "fill": "$background/background", + "children": [ + { + "id": "N7VQ33", + "type": "ref", + "ref": "kKFX9", + "height": "fill_container", + "descendants": { + "iTbd8": { + "fill": "#00000000" + }, + "iTbd8/ghUsw": { + "fill": "$foreground/foreground" + }, + "iTbd8/bpSQ7": { + "fill": "$foreground/foreground" + }, + "i8Z1B": { + "fill": "$accent/soft" + }, + "i8Z1B/Sr2Q5": { + "fill": "$accent/soft-foreground" + }, + "i8Z1B/Ra9NK": { + "fill": "$accent/soft-foreground" + } + } + }, + { + "type": "frame", + "id": "D9WMG", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$background/background", + "layout": "vertical", + "children": [ + { + "id": "dtGzG", + "type": "ref", + "ref": "gu5WY", + "width": "fill_container", + "descendants": { + "Gl7AY": { + "content": "mc-survival" + } + } + }, + { + "type": "frame", + "id": "cD0Oh", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "fill": "$background/background", + "layout": "vertical", + "gap": 24, + "padding": 24, + "children": [ + { + "id": "c1Y5Ok", + "type": "ref", + "ref": "S4k0x", + "name": "Detail Header", + "width": "fill_container" + }, + { + "id": "rERxz", + "type": "ref", + "ref": "I9kvlZ", + "name": "Tabs Bar", + "width": "fill_container", + "padding": [ + 0, + 0, + 0, + 24 + ], + "descendants": { + "xJNKQ": { + "width": "fill_container" + }, + "Wp6q1": { + "fill": "#00000000" + }, + "Wp6q1/YUR8N": { + "fill": "$muted" + }, + "Y4QES": { + "fill": "$segment" + }, + "Y4QES/VcxMI": { + "fill": "$foreground/segment" + } + } + }, + { + "type": "frame", + "id": "akpJ7", + "name": "Settings Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "id": "e3sKX1", + "type": "ref", + "ref": "EV8mp", + "name": "Settings Sub Nav", + "width": 224, + "height": "fill_container", + "descendants": { + "axMXL": { + "fill": "#00000000" + }, + "tv3iR": { + "fill": "$surface/secondary" + }, + "Y2PGH": { + "fill": "$foreground/foreground" + }, + "zpxa4": { + "fill": "$danger/danger" + }, + "xlVdh": { + "fill": "$danger/danger" + } + } + }, + { + "type": "frame", + "id": "gteRE", + "name": "Settings Panel Wrap", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "gKvSB", + "name": "Settings Form", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "MRC9x", + "name": "Form Body", + "width": "fill_container", + "layout": "vertical", + "gap": 24, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "i0diUb", + "name": "Field Auto-restart", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "RPw6X", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "kFRnr", + "name": "t", + "fill": "$foreground/foreground", + "content": "Auto-restart", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TbBL1", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "When off, the StatefulSet is scaled to zero (Suspended). Data is preserved.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A41uI", + "name": "ctrl", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "r0Bgr0", + "type": "ref", + "ref": "hl7R3", + "name": "Switch", + "width": 36, + "height": 20 + }, + { + "type": "text", + "id": "vkMHE", + "name": "state", + "fill": "$muted", + "content": "Pod is desired Running", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "su7N3", + "name": "Field Termination grace period", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "WBqSF", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "Xrd5T", + "name": "t", + "fill": "$foreground/foreground", + "content": "Termination grace period", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aTVe3", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "Seconds the operator waits for the template’s stop sequence to finish before scaling the pod down. Range 0–600; defaults to 30. No effect when the template declares no stop sequence.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Gbwcj", + "name": "ctrl", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "id": "YuFCv", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": 128, + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + }, + { + "type": "text", + "id": "P6ALv", + "name": "unit", + "fill": "$muted", + "content": "seconds", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EtIAm", + "name": "Field Health probes", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "yqHuR", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "n7xKbU", + "name": "t", + "fill": "$foreground/foreground", + "content": "Health probes", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q4kiTP", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "Per-server overrides for the template’s readiness/liveness/startup probe timing. The probe action is inherited from the template.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ks9Q9", + "name": "ctrl", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "wjKie", + "name": "Probe Readiness", + "width": "fill_container", + "cornerRadius": 4, + "stroke": "$border/border", + "strokeWidth": 1, + "strokeAlignment": "inner", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "CBahk", + "name": "hd", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vfg2j", + "name": "t", + "fill": "$foreground/foreground", + "content": "Readiness", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "HT0Re", + "name": "src", + "fill": "$muted", + "content": "from template", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R1Weyn", + "name": "row1", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "tvP6V", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "jGTHF", + "name": "l", + "fill": "$muted", + "content": "Initial delay", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "H9OMNH", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "ECVx6", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "HW9RA", + "name": "l", + "fill": "$muted", + "content": "Period", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "BeTsJ", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "10", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "yubmn", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "PlCZ0", + "name": "l", + "fill": "$muted", + "content": "Timeout", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "Bx782", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "it9JS", + "name": "row2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "uoXip", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "FwZDm", + "name": "l", + "fill": "$muted", + "content": "Failure threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "Hpmvn", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "20", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "gW08b", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "ai90E", + "name": "l", + "fill": "$muted", + "content": "Success threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "GkEPe", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + }, + { + "type": "frame", + "id": "B1kkIy", + "name": "spacer", + "width": "fill_container", + "height": 1, + "layout": "vertical" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Tz4II", + "name": "Probe Liveness", + "width": "fill_container", + "cornerRadius": 4, + "stroke": "$border/border", + "strokeWidth": 1, + "strokeAlignment": "inner", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "cizt9", + "name": "hd", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q6MgB0", + "name": "t", + "fill": "$foreground/foreground", + "content": "Liveness", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "m6C58", + "name": "src", + "fill": "$muted", + "content": "from template", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "dFjMA", + "name": "row1", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "PzhTY", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "sGd5t", + "name": "l", + "fill": "$muted", + "content": "Initial delay", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "h3PGEv", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "120", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "i0Rn2h", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "vPqFz", + "name": "l", + "fill": "$muted", + "content": "Period", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "uKAG5", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "GL1ul", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "c8iP0z", + "name": "l", + "fill": "$muted", + "content": "Timeout", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "a7nSw", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "I1zs4", + "name": "row2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "NeosO", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "KnKBw", + "name": "l", + "fill": "$muted", + "content": "Failure threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "T2CkHr", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "5", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "G5XRr0", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "FHCul", + "name": "l", + "fill": "$muted", + "content": "Success threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "v9Ffd1", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + }, + { + "type": "frame", + "id": "eQMtr", + "name": "spacer", + "width": "fill_container", + "height": 1, + "layout": "vertical" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fOzkm", + "name": "Probe Startup", + "width": "fill_container", + "cornerRadius": 4, + "stroke": "$border/border", + "strokeWidth": 1, + "strokeAlignment": "inner", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "iuV6w", + "name": "hd", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qrLM7", + "name": "t", + "fill": "$foreground/foreground", + "content": "Startup", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "FZbK1", + "name": "src", + "fill": "$muted", + "content": "from template", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CBNLQ", + "name": "row1", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "HrxxL", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "qCTus", + "name": "l", + "fill": "$muted", + "content": "Initial delay", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "W8v0A", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "ocMah", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "L6quPJ", + "name": "l", + "fill": "$muted", + "content": "Period", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "h1TxZ", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "10", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "G31fb", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "UlgTE", + "name": "l", + "fill": "$muted", + "content": "Timeout", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "nE6rz", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "vce4f", + "name": "row2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "POWVF", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "orqak", + "name": "l", + "fill": "$muted", + "content": "Failure threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "Act2V", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "60", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "HhDcM", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "u70Nfe", + "name": "l", + "fill": "$muted", + "content": "Success threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "rW31T", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + }, + { + "type": "frame", + "id": "CGJew", + "name": "spacer", + "width": "fill_container", + "height": 1, + "layout": "vertical" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x2uV0l", + "name": "Field Idle auto-sleep", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "Go2n7", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "KUny4", + "name": "t", + "fill": "$foreground/foreground", + "content": "Idle auto-sleep", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZDteX", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "Scale the server to zero when nobody is playing, and bring it back on a schedule or on demand. The world is saved first and the data volume is kept.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "onYBT", + "name": "ctrl", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "k08ihC", + "name": "Enable Row", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "DO3cG", + "type": "ref", + "ref": "hl7R3", + "name": "Switch" + }, + { + "type": "text", + "id": "j3HWkN", + "fill": "$foreground/foreground", + "content": "Enable idle auto-sleep", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "haDdS", + "name": "Sleep After Row", + "width": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XjsPN", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 80, + "content": "Sleep after", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "id": "pk7Ru", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": 100, + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + }, + { + "type": "text", + "id": "xPlLK", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "minutes of zero players (5–1440)", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N0t3r", + "name": "Wake Windows Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "hlGf8", + "fill": "$muted", + "content": "Wake windows", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "U3z6F", + "name": "Wake Window Row 1", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "a34vH", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "0 17 * * *", + "fill": "$foreground/foreground", + "fontFamily": "$typography/font-mono" + } + } + }, + { + "id": "f7oc1G", + "type": "ref", + "ref": "AaE0a", + "name": "Btn Remove", + "width": 36, + "height": 36, + "fill": "$background/background", + "stroke": "$border/border", + "descendants": { + "VjP01": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "AErDK", + "name": "Wake Window Row 2", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "MaeRd", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "0 9 * * 6,0", + "fill": "$foreground/foreground", + "fontFamily": "$typography/font-mono" + } + } + }, + { + "id": "eXpoW", + "type": "ref", + "ref": "AaE0a", + "name": "Btn Remove", + "width": 36, + "height": 36, + "fill": "$background/background", + "stroke": "$border/border", + "descendants": { + "VjP01": { + "icon": "x" + } + } + } + ] + }, + { + "id": "ZjmgT", + "type": "ref", + "ref": "rNhll", + "name": "Btn Add Wake Window", + "descendants": { + "wDXFJ/K8d1zg": { + "icon": "plus", + "library": "lucide" + }, + "wDXFJ/zMNkR": { + "content": "Add wake window" + } + } + }, + { + "type": "text", + "id": "EGTfR", + "fill": "$muted", + "content": "Standard five-field cron. Max 8 entries.", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lbmNu", + "name": "Wake On Connect Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "cWbJF", + "name": "Label", + "fill": "$muted", + "content": "Wake on connect", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZCWgU", + "name": "Wake On Connect Row", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "g3u7u", + "type": "ref", + "ref": "rh2QH", + "opacity": 0.5, + "name": "Switch", + "width": 36, + "height": 20 + }, + { + "type": "text", + "id": "DpDc6", + "name": "Switch Label", + "fill": "$muted", + "content": "Wake the server when a player connects", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "DJ7sb", + "name": "Helper", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Not available for this game: its template marks every port as unable to wake the server.", + "lineHeight": 1.4, + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oKWUe", + "name": "Caveat Row", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "a5SxI", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$muted" + }, + { + "type": "text", + "id": "mXBm5", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "A game that reports no player count will never sleep.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A4UnIb", + "name": "Caveat Row", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "uzUdN", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$muted" + }, + { + "type": "text", + "id": "T5ygp9", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "A wake window never restarts a server you stopped by hand.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "u23qy", + "name": "Form Footer", + "width": "fill_container", + "stroke": "$border/border", + "strokeWidth": { + "top": 1 + }, + "strokeAlignment": "inner", + "strokeLinejoin": "round", + "gap": 12, + "padding": [ + 12, + 24 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "E7b0jT", + "type": "ref", + "ref": "J09iP", + "name": "Btn Discard" + }, + { + "id": "o0UP1", + "type": "ref", + "ref": "z9ShNE", + "name": "Btn Save Changes", + "descendants": { + "nWTJb": { + "enabled": false + }, + "IVgQ8/U1xvSo": { + "content": "Save changes" + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/assets/design-export/json/i1bLR.json b/assets/design-export/json/i1bLR.json index 3c0fa6b25..34bba312f 100644 --- a/assets/design-export/json/i1bLR.json +++ b/assets/design-export/json/i1bLR.json @@ -1305,6 +1305,68 @@ } ] }, + { + "type": "frame", + "id": "Dz78Y", + "name": "Wake On Connect Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "y0KW6", + "name": "Label", + "fill": "$muted", + "content": "Wake on connect", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hSTTa", + "name": "Wake On Connect Row", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "V5vz6", + "type": "ref", + "ref": "rh2QH", + "name": "Switch", + "width": 36, + "height": 20 + }, + { + "type": "text", + "id": "B19SxE", + "name": "Switch Label", + "fill": "$foreground/foreground", + "content": "Wake the server when a player connects", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "ItxAU", + "name": "Helper", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "While the server sleeps, a small pod holds its ports and wakes it when a player tries to join. Costs one extra pod per sleeping server. Games that only use UDP wake on the first attempt; the player joins on retry.", + "lineHeight": 1.4, + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, { "type": "frame", "id": "C5gXq", diff --git a/assets/design-export/screenshots/A8vGwM.png b/assets/design-export/screenshots/A8vGwM.png new file mode 100644 index 000000000..893a63daf Binary files /dev/null and b/assets/design-export/screenshots/A8vGwM.png differ diff --git a/assets/design-export/screenshots/i1bLR.png b/assets/design-export/screenshots/i1bLR.png index 28d91178b..99fb6a9b4 100644 Binary files a/assets/design-export/screenshots/i1bLR.png and b/assets/design-export/screenshots/i1bLR.png differ diff --git a/design.pen b/design.pen index ee7f6091e..60dcac7fa 100644 --- a/design.pen +++ b/design.pen @@ -13635,6 +13635,68 @@ } ] }, + { + "type": "frame", + "id": "Dz78Y", + "name": "Wake On Connect Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "y0KW6", + "name": "Label", + "fill": "$muted", + "content": "Wake on connect", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hSTTa", + "name": "Wake On Connect Row", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "V5vz6", + "type": "ref", + "ref": "rh2QH", + "name": "Switch", + "width": 36, + "height": 20 + }, + { + "type": "text", + "id": "B19SxE", + "name": "Switch Label", + "fill": "$foreground/foreground", + "content": "Wake the server when a player connects", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "ItxAU", + "name": "Helper", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "While the server sleeps, a small pod holds its ports and wakes it when a player tries to join. Costs one extra pod per sleeping server. Games that only use UDP wake on the first attempt; the player joins on retry.", + "lineHeight": 1.4, + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, { "type": "frame", "id": "C5gXq", @@ -131611,6 +131673,1493 @@ ] } ] + }, + { + "type": "frame", + "id": "A8vGwM", + "x": 1640, + "y": 46130, + "name": "Screen/Server Detail — Settings · Lifecycle (Wake on connect unavailable)", + "theme": { + "semantic": "dark" + }, + "clip": true, + "width": 1440, + "height": 1620, + "fill": "$background/background", + "children": [ + { + "id": "N7VQ33", + "type": "ref", + "ref": "kKFX9", + "height": "fill_container", + "descendants": { + "iTbd8": { + "fill": "#00000000" + }, + "iTbd8/ghUsw": { + "fill": "$foreground/foreground" + }, + "iTbd8/bpSQ7": { + "fill": "$foreground/foreground" + }, + "i8Z1B": { + "fill": "$accent/soft" + }, + "i8Z1B/Sr2Q5": { + "fill": "$accent/soft-foreground" + }, + "i8Z1B/Ra9NK": { + "fill": "$accent/soft-foreground" + } + } + }, + { + "type": "frame", + "id": "D9WMG", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$background/background", + "layout": "vertical", + "children": [ + { + "id": "dtGzG", + "type": "ref", + "ref": "gu5WY", + "width": "fill_container", + "descendants": { + "Gl7AY": { + "content": "mc-survival" + } + } + }, + { + "type": "frame", + "id": "cD0Oh", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "fill": "$background/background", + "layout": "vertical", + "gap": 24, + "padding": 24, + "children": [ + { + "id": "c1Y5Ok", + "type": "ref", + "ref": "S4k0x", + "name": "Detail Header", + "width": "fill_container" + }, + { + "id": "rERxz", + "type": "ref", + "ref": "I9kvlZ", + "name": "Tabs Bar", + "width": "fill_container", + "padding": [ + 0, + 0, + 0, + 24 + ], + "descendants": { + "xJNKQ": { + "width": "fill_container" + }, + "Wp6q1": { + "fill": "#00000000" + }, + "Wp6q1/YUR8N": { + "fill": "$muted" + }, + "Y4QES": { + "fill": "$segment" + }, + "Y4QES/VcxMI": { + "fill": "$foreground/segment" + } + } + }, + { + "type": "frame", + "id": "akpJ7", + "name": "Settings Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "id": "e3sKX1", + "type": "ref", + "ref": "EV8mp", + "name": "Settings Sub Nav", + "width": 224, + "height": "fill_container", + "descendants": { + "axMXL": { + "fill": "#00000000" + }, + "tv3iR": { + "fill": "$surface/secondary" + }, + "Y2PGH": { + "fill": "$foreground/foreground" + }, + "zpxa4": { + "fill": "$danger/danger" + }, + "xlVdh": { + "fill": "$danger/danger" + } + } + }, + { + "type": "frame", + "id": "gteRE", + "name": "Settings Panel Wrap", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "gKvSB", + "name": "Settings Form", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "MRC9x", + "name": "Form Body", + "width": "fill_container", + "layout": "vertical", + "gap": 24, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "i0diUb", + "name": "Field Auto-restart", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "RPw6X", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "kFRnr", + "name": "t", + "fill": "$foreground/foreground", + "content": "Auto-restart", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TbBL1", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "When off, the StatefulSet is scaled to zero (Suspended). Data is preserved.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A41uI", + "name": "ctrl", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "r0Bgr0", + "type": "ref", + "ref": "hl7R3", + "name": "Switch", + "width": 36, + "height": 20 + }, + { + "type": "text", + "id": "vkMHE", + "name": "state", + "fill": "$muted", + "content": "Pod is desired Running", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "su7N3", + "name": "Field Termination grace period", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "WBqSF", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "Xrd5T", + "name": "t", + "fill": "$foreground/foreground", + "content": "Termination grace period", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aTVe3", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "Seconds the operator waits for the template’s stop sequence to finish before scaling the pod down. Range 0–600; defaults to 30. No effect when the template declares no stop sequence.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Gbwcj", + "name": "ctrl", + "width": "fill_container", + "gap": 10, + "alignItems": "center", + "children": [ + { + "id": "YuFCv", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": 128, + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + }, + { + "type": "text", + "id": "P6ALv", + "name": "unit", + "fill": "$muted", + "content": "seconds", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EtIAm", + "name": "Field Health probes", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "yqHuR", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "n7xKbU", + "name": "t", + "fill": "$foreground/foreground", + "content": "Health probes", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q4kiTP", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "Per-server overrides for the template’s readiness/liveness/startup probe timing. The probe action is inherited from the template.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ks9Q9", + "name": "ctrl", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "wjKie", + "name": "Probe Readiness", + "width": "fill_container", + "cornerRadius": 4, + "stroke": "$border/border", + "strokeWidth": 1, + "strokeAlignment": "inner", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "CBahk", + "name": "hd", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vfg2j", + "name": "t", + "fill": "$foreground/foreground", + "content": "Readiness", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "HT0Re", + "name": "src", + "fill": "$muted", + "content": "from template", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R1Weyn", + "name": "row1", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "tvP6V", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "jGTHF", + "name": "l", + "fill": "$muted", + "content": "Initial delay", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "H9OMNH", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "ECVx6", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "HW9RA", + "name": "l", + "fill": "$muted", + "content": "Period", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "BeTsJ", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "10", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "yubmn", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "PlCZ0", + "name": "l", + "fill": "$muted", + "content": "Timeout", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "Bx782", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "it9JS", + "name": "row2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "uoXip", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "FwZDm", + "name": "l", + "fill": "$muted", + "content": "Failure threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "Hpmvn", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "20", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "gW08b", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "ai90E", + "name": "l", + "fill": "$muted", + "content": "Success threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "GkEPe", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + }, + { + "type": "frame", + "id": "B1kkIy", + "name": "spacer", + "width": "fill_container", + "height": 1, + "layout": "vertical" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Tz4II", + "name": "Probe Liveness", + "width": "fill_container", + "cornerRadius": 4, + "stroke": "$border/border", + "strokeWidth": 1, + "strokeAlignment": "inner", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "cizt9", + "name": "hd", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q6MgB0", + "name": "t", + "fill": "$foreground/foreground", + "content": "Liveness", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "m6C58", + "name": "src", + "fill": "$muted", + "content": "from template", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "dFjMA", + "name": "row1", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "PzhTY", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "sGd5t", + "name": "l", + "fill": "$muted", + "content": "Initial delay", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "h3PGEv", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "120", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "i0Rn2h", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "vPqFz", + "name": "l", + "fill": "$muted", + "content": "Period", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "uKAG5", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "GL1ul", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "c8iP0z", + "name": "l", + "fill": "$muted", + "content": "Timeout", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "a7nSw", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "I1zs4", + "name": "row2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "NeosO", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "KnKBw", + "name": "l", + "fill": "$muted", + "content": "Failure threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "T2CkHr", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "5", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "G5XRr0", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "FHCul", + "name": "l", + "fill": "$muted", + "content": "Success threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "v9Ffd1", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + }, + { + "type": "frame", + "id": "eQMtr", + "name": "spacer", + "width": "fill_container", + "height": 1, + "layout": "vertical" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fOzkm", + "name": "Probe Startup", + "width": "fill_container", + "cornerRadius": 4, + "stroke": "$border/border", + "strokeWidth": 1, + "strokeAlignment": "inner", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "iuV6w", + "name": "hd", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qrLM7", + "name": "t", + "fill": "$foreground/foreground", + "content": "Startup", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "FZbK1", + "name": "src", + "fill": "$muted", + "content": "from template", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CBNLQ", + "name": "row1", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "HrxxL", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "qCTus", + "name": "l", + "fill": "$muted", + "content": "Initial delay", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "W8v0A", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "ocMah", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "L6quPJ", + "name": "l", + "fill": "$muted", + "content": "Period", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "h1TxZ", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "10", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "G31fb", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "UlgTE", + "name": "l", + "fill": "$muted", + "content": "Timeout", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "nE6rz", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "vce4f", + "name": "row2", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "POWVF", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "orqak", + "name": "l", + "fill": "$muted", + "content": "Failure threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "Act2V", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "60", + "fill": "$foreground/foreground" + } + } + } + ] + }, + { + "type": "frame", + "id": "HhDcM", + "name": "f", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "u70Nfe", + "name": "l", + "fill": "$muted", + "content": "Success threshold", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "id": "rW31T", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "—", + "fill": "#5C5C5C" + } + } + } + ] + }, + { + "type": "frame", + "id": "CGJew", + "name": "spacer", + "width": "fill_container", + "height": 1, + "layout": "vertical" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x2uV0l", + "name": "Field Idle auto-sleep", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "Go2n7", + "name": "lbl", + "width": 200, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "KUny4", + "name": "t", + "fill": "$foreground/foreground", + "content": "Idle auto-sleep", + "fontFamily": "$typography/font-sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZDteX", + "name": "h", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 200, + "content": "Scale the server to zero when nobody is playing, and bring it back on a schedule or on demand. The world is saved first and the data volume is kept.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "onYBT", + "name": "ctrl", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "k08ihC", + "name": "Enable Row", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "DO3cG", + "type": "ref", + "ref": "hl7R3", + "name": "Switch" + }, + { + "type": "text", + "id": "j3HWkN", + "fill": "$foreground/foreground", + "content": "Enable idle auto-sleep", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "haDdS", + "name": "Sleep After Row", + "width": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XjsPN", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 80, + "content": "Sleep after", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "id": "pk7Ru", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": 100, + "descendants": { + "PJERm": { + "content": "30", + "fill": "$foreground/foreground" + } + } + }, + { + "type": "text", + "id": "xPlLK", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "minutes of zero players (5–1440)", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N0t3r", + "name": "Wake Windows Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "hlGf8", + "fill": "$muted", + "content": "Wake windows", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "U3z6F", + "name": "Wake Window Row 1", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "a34vH", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "0 17 * * *", + "fill": "$foreground/foreground", + "fontFamily": "$typography/font-mono" + } + } + }, + { + "id": "f7oc1G", + "type": "ref", + "ref": "AaE0a", + "name": "Btn Remove", + "width": 36, + "height": 36, + "fill": "$background/background", + "stroke": "$border/border", + "descendants": { + "VjP01": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "AErDK", + "name": "Wake Window Row 2", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "MaeRd", + "type": "ref", + "ref": "D0cDM", + "name": "Input", + "width": "fill_container", + "descendants": { + "PJERm": { + "content": "0 9 * * 6,0", + "fill": "$foreground/foreground", + "fontFamily": "$typography/font-mono" + } + } + }, + { + "id": "eXpoW", + "type": "ref", + "ref": "AaE0a", + "name": "Btn Remove", + "width": 36, + "height": 36, + "fill": "$background/background", + "stroke": "$border/border", + "descendants": { + "VjP01": { + "icon": "x" + } + } + } + ] + }, + { + "id": "ZjmgT", + "type": "ref", + "ref": "rNhll", + "name": "Btn Add Wake Window", + "descendants": { + "wDXFJ/K8d1zg": { + "icon": "plus", + "library": "lucide" + }, + "wDXFJ/zMNkR": { + "content": "Add wake window" + } + } + }, + { + "type": "text", + "id": "EGTfR", + "fill": "$muted", + "content": "Standard five-field cron. Max 8 entries.", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lbmNu", + "name": "Wake On Connect Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "cWbJF", + "name": "Label", + "fill": "$muted", + "content": "Wake on connect", + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZCWgU", + "name": "Wake On Connect Row", + "width": "fill_container", + "height": 28, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "g3u7u", + "type": "ref", + "ref": "rh2QH", + "opacity": 0.5, + "name": "Switch", + "width": 36, + "height": 20 + }, + { + "type": "text", + "id": "DpDc6", + "name": "Switch Label", + "fill": "$muted", + "content": "Wake the server when a player connects", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "DJ7sb", + "name": "Helper", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Not available for this game: its template marks every port as unable to wake the server.", + "lineHeight": 1.4, + "fontFamily": "$typography/font-sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oKWUe", + "name": "Caveat Row", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "a5SxI", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$muted" + }, + { + "type": "text", + "id": "mXBm5", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "A game that reports no player count will never sleep.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A4UnIb", + "name": "Caveat Row", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "uzUdN", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$muted" + }, + { + "type": "text", + "id": "T5ygp9", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "A wake window never restarts a server you stopped by hand.", + "fontFamily": "$typography/font-sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "u23qy", + "name": "Form Footer", + "width": "fill_container", + "stroke": "$border/border", + "strokeWidth": { + "top": 1 + }, + "strokeAlignment": "inner", + "strokeLinejoin": "round", + "gap": 12, + "padding": [ + 12, + 24 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "E7b0jT", + "type": "ref", + "ref": "J09iP", + "name": "Btn Discard" + }, + { + "id": "o0UP1", + "type": "ref", + "ref": "z9ShNE", + "name": "Btn Save Changes", + "descendants": { + "nWTJb": { + "enabled": false + }, + "IVgQ8/U1xvSo": { + "content": "Save changes" + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] } ], "themes": { diff --git a/i18n/locales/en/app.json b/i18n/locales/en/app.json index 560ed3397..4df6d72e9 100644 --- a/i18n/locales/en/app.json +++ b/i18n/locales/en/app.json @@ -9075,6 +9075,18 @@ "defaultMessage": "Termination grace period", "description": "Lifecycle: field label for the number of seconds given to the server to stop cleanly before it is shut down" }, + "web.routes.tabs.settings.lifecycle.wake_on_connect": { + "defaultMessage": "Wake on connect", + "description": "Lifecycle: small label above the switch that wakes a sleeping server when a player tries to join" + }, + "web.routes.tabs.settings.lifecycle.wake_on_connect_not_available": { + "defaultMessage": "Not available for this game: its template marks every port as unable to wake the server.", + "description": "Lifecycle: hint under the disabled wake on connect switch when the game's template has no port that can wake the server" + }, + "web.routes.tabs.settings.lifecycle.wake_the_server_when_a_player_connects": { + "defaultMessage": "Wake the server when a player connects", + "description": "Lifecycle: text next to the wake on connect switch, also its accessible name; the server wakes from sleep when a player tries to join" + }, "web.routes.tabs.settings.lifecycle.wake_window_aria": { "defaultMessage": "Wake window {n}", "description": "Lifecycle: aria-label of a wake window cron input; {n} is the 1-based position of the window" @@ -9095,6 +9107,10 @@ "defaultMessage": "When off, the StatefulSet is scaled to zero (Suspended). Data is preserved.", "description": "Lifecycle: hint under the auto-restart switch saying that when it is off the server is suspended and its data is kept" }, + "web.routes.tabs.settings.lifecycle.while_the_server_sleeps_a_small_pod": { + "defaultMessage": "While the server sleeps, a small pod holds its ports and wakes it when a player tries to join. Costs one extra pod per sleeping server. Games that only use UDP wake on the first attempt; the player joins on retry.", + "description": "Lifecycle: hint under the wake on connect switch explaining that a small pod holds the ports while asleep, that it costs one pod per sleeping server, and that for UDP-only games the first join attempt only wakes the server" + }, "web.routes.tabs.settings.loading": { "defaultMessage": "Loading…", "description": "Settings tab: loading text shown while the settings are fetched or a settings section is still loading" diff --git a/i18n/locales/fr/app.json b/i18n/locales/fr/app.json index 854bd6fe9..ae08277b0 100644 --- a/i18n/locales/fr/app.json +++ b/i18n/locales/fr/app.json @@ -11334,6 +11334,21 @@ "description": "Lifecycle: field label for the number of seconds given to the server to stop cleanly before it is shut down", "sourceHash": "5a2eee9ddf20" }, + "web.routes.tabs.settings.lifecycle.wake_on_connect": { + "defaultMessage": "Réveil à la connexion", + "description": "Lifecycle: small label above the switch that wakes a sleeping server when a player tries to join", + "sourceHash": "199fb74715af" + }, + "web.routes.tabs.settings.lifecycle.wake_on_connect_not_available": { + "defaultMessage": "Indisponible pour ce jeu : son template indique qu'aucun port ne peut réveiller le serveur.", + "description": "Lifecycle: hint under the disabled wake on connect switch when the game's template has no port that can wake the server", + "sourceHash": "d5560339da80" + }, + "web.routes.tabs.settings.lifecycle.wake_the_server_when_a_player_connects": { + "defaultMessage": "Réveiller le serveur quand un joueur se connecte", + "description": "Lifecycle: text next to the wake on connect switch, also its accessible name; the server wakes from sleep when a player tries to join", + "sourceHash": "8e5b61c242e5" + }, "web.routes.tabs.settings.lifecycle.wake_window_aria": { "defaultMessage": "Fenêtre de réveil {n}", "description": "Lifecycle: aria-label of a wake window cron input; {n} is the 1-based position of the window", @@ -11359,6 +11374,11 @@ "description": "Lifecycle: hint under the auto-restart switch saying that when it is off the server is suspended and its data is kept", "sourceHash": "9a0aa8691eb4" }, + "web.routes.tabs.settings.lifecycle.while_the_server_sleeps_a_small_pod": { + "defaultMessage": "Pendant que le serveur est en veille, un petit pod garde ses ports et le réveille quand un joueur essaie de se connecter. Coûte un pod de plus par serveur en veille. Les jeux qui n'utilisent que l'UDP se réveillent à la première tentative ; le joueur se connecte en réessayant.", + "description": "Lifecycle: hint under the wake on connect switch explaining that a small pod holds the ports while asleep, that it costs one pod per sleeping server, and that for UDP-only games the first join attempt only wakes the server", + "sourceHash": "0ff093fbfd8d" + }, "web.routes.tabs.settings.loading": { "defaultMessage": "Chargement…", "description": "Settings tab: loading text shown while the settings are fetched or a settings section is still loading", diff --git a/web/specs.md b/web/specs.md index 7cb5481af..02893167c 100644 --- a/web/specs.md +++ b/web/specs.md @@ -412,7 +412,7 @@ All 11 sections render form controls from HeroUI (TextField, Select, Slider, Swi 3. **Resources** (`web/src/routes/tabs/settings/Resources.tsx`) — CPU request/limit, memory request/limit, storage size sliders + TextField inputs 4. **Networking** (`web/src/routes/tabs/settings/Networking.tsx`) — Port table (external/internal/protocol), address pool picker, firewall rules via HeroUI Table + Modal dialogs 5. **Environment** (`web/src/routes/tabs/settings/EnvVars.tsx`) — Environment variable key/value editor (table, add/delete rows) via HeroUI Table + TextField -6. **Lifecycle** (`web/src/routes/tabs/settings/Lifecycle.tsx`) — Auto-pause threshold, idle sleep settings, quiesce strategy selector via HeroUI Switch/Select/Slider +6. **Lifecycle** (`web/src/routes/tabs/settings/Lifecycle.tsx`) — Auto-pause threshold, idle sleep settings, quiesce strategy selector via HeroUI Switch/Select/Slider. Inside the idle settings, a "Wake on connect" switch writes `spec.idle.wakeOnConnect`, which arms the wake sentinel. It is disabled when the template advertises no port whose `wakeProtocol` can wake the server (every advertised port is `none`, mirroring the operator's `wakeOnConnectEligible`), unless the server is already armed, so it can still be turned off. Switching idle auto-sleep off and on keeps `wakeOnConnect`. 7. **Scheduled backups** (`web/src/routes/tabs/settings/Backups.tsx`) — Retention days selector, retention policy dropdown via HeroUI Select/TextField 8. **Network capture** (`web/src/routes/tabs/settings/NetworkCapture.tsx`) — Capture enabled toggle, BPF filter input, retention policy via HeroUI Switch/TextField/Select; warning banner via ui/ component 9. **Placement** (`web/src/routes/tabs/settings/Placement.tsx`) — Node affinity rules, pod-node-selector builder via HeroUI form components diff --git a/web/src/routes/tabs/settings/Lifecycle.test.tsx b/web/src/routes/tabs/settings/Lifecycle.test.tsx index bf3f07c5c..bd64c5acf 100644 --- a/web/src/routes/tabs/settings/Lifecycle.test.tsx +++ b/web/src/routes/tabs/settings/Lifecycle.test.tsx @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event"; import { LifecycleSection } from "./Lifecycle"; import { GRACE_PERIOD_ANNOTATION } from "./types"; import { makeServer } from "@/test/factories"; -import type { GameTemplate } from "@/types"; +import type { GameTemplate, IdleSpec } from "@/types"; const baseDraft = makeServer(); @@ -520,4 +520,114 @@ describe("LifecycleSection", () => { ); expect(screen.queryByText(/between 5 and 1440/i)).not.toBeInTheDocument(); }); + describe("wake on connect", () => { + const idleOn = { enabled: true, afterMinutes: 45, wakeWindows: ["0 9 * * 1-5"] }; + const withIdle = (idle: IdleSpec) => ({ + ...baseDraft, + spec: { ...baseDraft.spec, idle }, + }); + const templateWithPorts = ( + ports: NonNullable, + ): GameTemplate => ({ ...templateWithProbe, spec: { ...templateWithProbe.spec, ports } }); + const switchName = "Wake the server when a player connects"; + + it("arms the wake sentinel and keeps the other idle settings", async () => { + const onChange = vi.fn(); + render(); + await userEvent.click(screen.getByRole("switch", { name: switchName })); + const lastCall = onChange.mock.calls.at(-1)![0]; + expect(lastCall.spec.idle).toEqual({ ...idleOn, wakeOnConnect: true }); + }); + + it("disarms it again", async () => { + const onChange = vi.fn(); + render( + , + ); + const sw = screen.getByRole("switch", { name: switchName }); + expect(sw).toBeChecked(); + await userEvent.click(sw); + const lastCall = onChange.mock.calls.at(-1)![0]; + expect(lastCall.spec.idle?.wakeOnConnect).toBe(false); + }); + + it("survives switching idle auto-sleep off and on again", async () => { + // Regression: re-enabling rebuilt spec.idle from afterMinutes and + // wakeWindows only, silently dropping wakeOnConnect. + const onChange = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole("switch", { name: "Enable idle auto-sleep" })); + const lastCall = onChange.mock.calls.at(-1)![0]; + expect(lastCall.spec.idle?.enabled).toBe(true); + expect(lastCall.spec.idle?.wakeOnConnect).toBe(true); + }); + + it("explains the extra pod and the UDP retry", () => { + render( {}} />); + expect(screen.getByText(/a small pod holds its ports/)).toBeInTheDocument(); + expect(screen.getByText(/the player joins on retry/)).toBeInTheDocument(); + }); + + it("is available when the template has an advertised port that can wake the server", () => { + render( + {}} + template={templateWithPorts([ + { name: "query", containerPort: 27015, protocol: "UDP", advertise: false, wakeProtocol: "none" }, + // No wakeProtocol: the CRD defaults it to "generic". + { name: "game", containerPort: 8211, protocol: "UDP", advertise: true }, + ])} + />, + ); + expect(screen.getByRole("switch", { name: switchName })).not.toBeDisabled(); + expect(screen.queryByText(/Not available for this game/)).not.toBeInTheDocument(); + }); + + it("is disabled when no advertised port can wake the server", () => { + render( + {}} + template={templateWithPorts([ + { name: "game", containerPort: 7777, protocol: "UDP", advertise: true, wakeProtocol: "none" }, + { name: "rcon", containerPort: 25575, protocol: "TCP", advertise: false, wakeProtocol: "generic" }, + ])} + />, + ); + expect(screen.getByRole("switch", { name: switchName })).toBeDisabled(); + expect( + screen.getByText("Not available for this game: its template marks every port as unable to wake the server."), + ).toBeInTheDocument(); + }); + + it("is disabled when the loaded template declares no ports", () => { + // The operator finds no port to wake on, so it never arms the sentinel. + render( {}} template={templateWithProbe} />); + expect(screen.getByRole("switch", { name: switchName })).toBeDisabled(); + expect(screen.getByText(/Not available for this game/)).toBeInTheDocument(); + }); + + it("still lets an already armed server be disarmed when the template cannot wake it", async () => { + const onChange = vi.fn(); + render( + , + ); + const sw = screen.getByRole("switch", { name: switchName }); + expect(sw).not.toBeDisabled(); + await userEvent.click(sw); + expect(onChange.mock.calls.at(-1)![0].spec.idle?.wakeOnConnect).toBe(false); + }); + }); }); diff --git a/web/src/routes/tabs/settings/Lifecycle.tsx b/web/src/routes/tabs/settings/Lifecycle.tsx index 78f7b5c3f..18c1a4e76 100644 --- a/web/src/routes/tabs/settings/Lifecycle.tsx +++ b/web/src/routes/tabs/settings/Lifecycle.tsx @@ -52,6 +52,15 @@ export function LifecycleSection({ draft, onChange, template }: SectionProps) { : (annotations[GRACE_PERIOD_ANNOTATION] ?? ""); const templateProbes = template?.spec.probes; const overrides = draft.spec.probes; + // Mirrors the operator's wakeOnConnectEligible: the wake sentinel is only + // armed when the template advertises a port that can wake the server + // (wakeProtocol other than "none"; the CRD defaults it to "generic"). The + // switch stays available until the template has loaded; a loaded template + // without ports has nothing that can wake the server. + const wakeAvailable = + template === undefined || + (template.spec.ports ?? []).some((p) => p.advertise && p.wakeProtocol !== "none"); + const wakeOnConnect = draft.spec.idle?.wakeOnConnect ?? false; // setProbe writes a per-server override for `kind`, seeded from the // template probe so the action (httpGet/tcpSocket/exec) is preserved. @@ -159,7 +168,18 @@ export function LifecycleSection({ draft, onChange, template }: SectionProps) { }); return; } - setIdle({ enabled: true, afterMinutes: idle?.afterMinutes ?? 30, wakeWindows: idle?.wakeWindows ?? [] }); + // Spread the saved settings first so fields this form sets elsewhere, + // such as wakeOnConnect, survive switching auto-sleep off and on again. + setIdle({ ...idle, enabled: true, afterMinutes: idle?.afterMinutes ?? 30, wakeWindows: idle?.wakeWindows ?? [] }); + }; + + /** Arms or disarms the wake sentinel (spec.idle.wakeOnConnect). */ + const setWakeOnConnect = (on: boolean) => { + const idle = draft.spec.idle ?? { enabled: true }; + setIdle({ + ...idle, + wakeOnConnect: on, + }); }; const setIdleAfterMinutes = (value: string) => { @@ -363,6 +383,32 @@ export function LifecycleSection({ draft, onChange, template }: SectionProps) { {intl.formatMessage({ id: "web.routes.tabs.settings.lifecycle.standard_five_field_cron_max_8_entries", defaultMessage: "Standard five-field cron. Max 8 entries.", description: "Lifecycle: hint under the wake windows list saying the entries use standard five-field cron and at most 8 are allowed" })} +
+ +
+ + + + + + + + {intl.formatMessage({ id: "web.routes.tabs.settings.lifecycle.wake_the_server_when_a_player_connects", defaultMessage: "Wake the server when a player connects", description: "Lifecycle: text next to the wake on connect switch, also its accessible name; the server wakes from sleep when a player tries to join" })} +
+
+ {wakeAvailable + ? intl.formatMessage({ id: "web.routes.tabs.settings.lifecycle.while_the_server_sleeps_a_small_pod", defaultMessage: "While the server sleeps, a small pod holds its ports and wakes it when a player tries to join. Costs one extra pod per sleeping server. Games that only use UDP wake on the first attempt; the player joins on retry.", description: "Lifecycle: hint under the wake on connect switch explaining that a small pod holds the ports while asleep, that it costs one pod per sleeping server, and that for UDP-only games the first join attempt only wakes the server" }) + : intl.formatMessage({ id: "web.routes.tabs.settings.lifecycle.wake_on_connect_not_available", defaultMessage: "Not available for this game: its template marks every port as unable to wake the server.", description: "Lifecycle: hint under the disabled wake on connect switch when the game's template has no port that can wake the server" })} +
+
+
{intl.formatMessage({ id: "web.routes.tabs.settings.lifecycle.a_game_that_reports_no_player_count", defaultMessage: "A game that reports no player count will never sleep.", description: "Lifecycle: warning note that a game reporting no player count will never sleep, since idle detection cannot work" })}
{intl.formatMessage({ id: "web.routes.tabs.settings.lifecycle.a_wake_window_never_restarts_a_server", defaultMessage: "A wake window never restarts a server you stopped by hand.", description: "Lifecycle: note that a wake window does not restart a server the user stopped by hand" })}
diff --git a/web/src/types.ts b/web/src/types.ts index 880c2b72c..1832222bc 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -307,6 +307,16 @@ export interface GameTemplate { accentColor?: string; image: string; versions?: GameVersion[]; + // Ports the game listens on. wakeProtocol picks the wake sentinel's parser + // for the port while the server sleeps; the CRD defaults it to "generic", + // and "none" means the port never wakes the server. + ports?: Array<{ + name: string; + containerPort: number; + protocol?: string; + advertise: boolean; + wakeProtocol?: "minecraft" | "terraria" | "generic" | "none"; + }>; logPath?: string; consoleMode?: "rcon" | "pty" | "none"; // authentication: whether the console requires a password (default true).