Jelajahi Sumber

Persist editable startup flags

Store runtime-editable startup flags in the system database and restore them during boot, while keeping explicit command-line flags authoritative for that run. This also adds validation and tests for flag merging, and refreshes the SystemAO runtime page to reflect the new persistent behavior with clearer controls and updated localized strings.
Toby Chui 1 Minggu lalu
induk
melakukan
fd1fdd687f

+ 180 - 38
src/startup.flags.go

@@ -2,7 +2,10 @@ package main
 
 import (
 	"encoding/json"
+	"errors"
+	"flag"
 	"net/http"
+	"strings"
 
 	prout "imuslab.com/arozos/mod/prouter"
 	"imuslab.com/arozos/mod/utils"
@@ -12,10 +15,158 @@ import (
 	Startup Flags Manager
 
 	This script is design to provide interface for editing the boot flags
-	during the system is running
+	during the system is running.
 
+	Changes are applied to the runtime and persisted to the system database,
+	so they survive a restart. On boot, StartupFlagsRestore loads the stored
+	values into the runtime, except for the ones given explicitly as start
+	parameters: those win, and overwrite the stored value.
 */
 
+const bootFlagsTable = "bootflags"
+const bootFlagsKey = "config"
+
+// bootFlags is the persisted and editable subset of the startup parameters.
+// MaxUploadSize and MaxFileUploadBuff are in MB, FileIOBuffer in bytes.
+type bootFlags struct {
+	Hostname          string
+	MaxUploadSize     int
+	MaxFileUploadBuff int
+	FileIOBuffer      int
+	DisableIPResolver bool
+	EnableHomePage    bool
+	EnableDirListing  bool
+}
+
+// bootFlagNames maps each bootFlags field to the command line flag it mirrors
+var bootFlagNames = map[string]string{
+	"Hostname":          "hostname",
+	"MaxUploadSize":     "max_upload_size",
+	"MaxFileUploadBuff": "upload_buf",
+	"FileIOBuffer":      "iobuf",
+	"DisableIPResolver": "disable_ip_resolver",
+	"EnableHomePage":    "homepage",
+	"EnableDirListing":  "dir_list",
+}
+
+// currentBootFlags reads the boot flags from the runtime globals
+func currentBootFlags() bootFlags {
+	return bootFlags{
+		Hostname:          *host_name,
+		MaxUploadSize:     int(max_upload_size >> 20),
+		MaxFileUploadBuff: *upload_buf,
+		FileIOBuffer:      *file_opr_buff,
+		DisableIPResolver: *disable_ip_resolve_services,
+		EnableHomePage:    *allow_homepage,
+		EnableDirListing:  *enable_dir_listing,
+	}
+}
+
+// applyBootFlags writes the boot flags into the runtime globals
+func applyBootFlags(c bootFlags) {
+	*host_name = c.Hostname
+	*max_upload = c.MaxUploadSize
+	max_upload_size = int64(c.MaxUploadSize) << 20
+	*upload_buf = c.MaxFileUploadBuff
+	*file_opr_buff = c.FileIOBuffer
+	*disable_ip_resolve_services = c.DisableIPResolver
+	*allow_homepage = c.EnableHomePage
+	*enable_dir_listing = c.EnableDirListing
+}
+
+// validateBootFlags normalises and checks a config before it is applied
+func validateBootFlags(c *bootFlags) error {
+	c.Hostname = strings.TrimSpace(c.Hostname)
+	if c.Hostname == "" {
+		return errors.New("host name cannot be empty")
+	}
+	if c.MaxUploadSize <= 0 {
+		return errors.New("max upload size must be larger than zero")
+	}
+	if c.MaxFileUploadBuff <= 0 {
+		return errors.New("file upload buffer must be larger than zero")
+	}
+	if c.FileIOBuffer <= 0 {
+		return errors.New("file IO buffer must be larger than zero")
+	}
+	return nil
+}
+
+// explicitStartFlags returns the names of the flags given on the command line
+func explicitStartFlags() map[string]bool {
+	set := map[string]bool{}
+	flag.Visit(func(f *flag.Flag) {
+		set[f.Name] = true
+	})
+	return set
+}
+
+/*
+mergeBootFlags decides the boot flags to run with. Fields whose flag was given
+explicitly keep the runtime value, other fields take the stored value when one
+exists (and is valid), and fall back to the runtime default otherwise. stored
+is kept as raw fields so a config saved by an older version, missing some
+fields, only restores the ones it has.
+*/
+func mergeBootFlags(runtime bootFlags, stored map[string]json.RawMessage, explicit map[string]bool) bootFlags {
+	merged := runtime
+	restore := func(field string, target interface{}) {
+		raw, ok := stored[field]
+		if !ok || explicit[bootFlagNames[field]] {
+			return
+		}
+		json.Unmarshal(raw, target)
+	}
+
+	restore("Hostname", &merged.Hostname)
+	restore("MaxUploadSize", &merged.MaxUploadSize)
+	restore("MaxFileUploadBuff", &merged.MaxFileUploadBuff)
+	restore("FileIOBuffer", &merged.FileIOBuffer)
+	restore("DisableIPResolver", &merged.DisableIPResolver)
+	restore("EnableHomePage", &merged.EnableHomePage)
+	restore("EnableDirListing", &merged.EnableDirListing)
+
+	//A damaged stored value must not stop the system from booting
+	if strings.TrimSpace(merged.Hostname) == "" {
+		merged.Hostname = runtime.Hostname
+	}
+	if merged.MaxUploadSize <= 0 {
+		merged.MaxUploadSize = runtime.MaxUploadSize
+	}
+	if merged.MaxFileUploadBuff <= 0 {
+		merged.MaxFileUploadBuff = runtime.MaxFileUploadBuff
+	}
+	if merged.FileIOBuffer <= 0 {
+		merged.FileIOBuffer = runtime.FileIOBuffer
+	}
+	return merged
+}
+
+/*
+StartupFlagsRestore loads the persisted boot flags into the runtime. It must
+run right after the system database opens and before any service reads these
+flags (e.g. the host name is captured by several modules at init).
+*/
+func StartupFlagsRestore() {
+	sysdb.NewTable(bootFlagsTable)
+
+	stored := map[string]json.RawMessage{}
+	if sysdb.KeyExists(bootFlagsTable, bootFlagsKey) {
+		if err := sysdb.Read(bootFlagsTable, bootFlagsKey, &stored); err != nil {
+			systemWideLogger.PrintAndLog("System", "Unable to read stored startup parameters, using defaults", err)
+			stored = map[string]json.RawMessage{}
+		}
+	}
+
+	merged := mergeBootFlags(currentBootFlags(), stored, explicitStartFlags())
+	applyBootFlags(merged)
+
+	//Write back so the start flags given this boot overwrite the stored values
+	if err := sysdb.Write(bootFlagsTable, bootFlagsKey, merged); err != nil {
+		systemWideLogger.PrintAndLog("System", "Unable to persist startup parameters", err)
+	}
+}
+
 func StartupFlagsInit() {
 	//Create a admin permission router for handling requests
 	//Register a boot flag modifier
@@ -40,27 +191,22 @@ func StartupFlagsInit() {
 }
 
 func handleBootFlagsFunction(w http.ResponseWriter, r *http.Request) {
-	type bootFlags struct {
-		Hostname          string
-		MaxUploadSize     int
-		MaxFileUploadBuff int
-		FileIOBuffer      int
-		DisableIPResolver bool
-		EnableHomePage    bool
-		EnableDirListing  bool
-	}
 	opr, _ := utils.PostPara(r, "opr")
 	if opr == "" {
-		//List the current boot flag, all units in MB
-		js, _ := json.Marshal(bootFlags{
-			*host_name,
-			int(max_upload_size >> 20),
-			*upload_buf,
-			*file_opr_buff,
-			*disable_ip_resolve_services,
-			*allow_homepage,
-			*enable_dir_listing,
-		})
+		//List the current boot flags, together with the fields that were
+		//given as start parameters this boot (they override the stored value)
+		explicit := explicitStartFlags()
+		fromStartFlag := []string{}
+		for field, name := range bootFlagNames {
+			if explicit[name] {
+				fromStartFlag = append(fromStartFlag, field)
+			}
+		}
+
+		js, _ := json.Marshal(struct {
+			bootFlags
+			FromStartFlag []string
+		}{currentBootFlags(), fromStartFlag})
 
 		utils.SendJSONResponse(w, string(js))
 	} else if opr == "set" {
@@ -71,31 +217,27 @@ func handleBootFlagsFunction(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		//Try parse it
-		newConfig := bootFlags{
-			"My ArOZ",
-			8192,
-			25,
-			1024,
-			false,
-			false,
-			true,
-		}
+		//Fields missing from the request keep their current value
+		newConfig := currentBootFlags()
 		err = json.Unmarshal([]byte(newSettings), &newConfig)
 		if err != nil {
 			utils.SendErrorResponse(w, err.Error())
 			return
 		}
 
-		//Update the current global flags
+		if err = validateBootFlags(&newConfig); err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Update the current global flags and persist them for the next boot
 		systemWideLogger.PrintAndLog("System", "Updating boot flag to:"+newSettings, nil)
-		*host_name = newConfig.Hostname
-		max_upload_size = int64(newConfig.MaxUploadSize << 20)
-		*upload_buf = newConfig.MaxFileUploadBuff
-		*file_opr_buff = newConfig.FileIOBuffer
-		*disable_ip_resolve_services = newConfig.DisableIPResolver
-		*allow_homepage = newConfig.EnableHomePage
-		*enable_dir_listing = newConfig.EnableDirListing
+		applyBootFlags(newConfig)
+		if err = sysdb.Write(bootFlagsTable, bootFlagsKey, newConfig); err != nil {
+			systemWideLogger.PrintAndLog("System", "Unable to persist startup parameters", err)
+			utils.SendErrorResponse(w, "Applied to runtime but failed to save: "+err.Error())
+			return
+		}
 
 		utils.SendOK(w)
 	} else {

+ 165 - 0
src/startup.flags_test.go

@@ -0,0 +1,165 @@
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"testing"
+)
+
+func testDefaultBootFlags() bootFlags {
+	return bootFlags{
+		Hostname:          "My ArOZ",
+		MaxUploadSize:     8192,
+		MaxFileUploadBuff: 25,
+		FileIOBuffer:      1024,
+		DisableIPResolver: false,
+		EnableHomePage:    true,
+		EnableDirListing:  true,
+	}
+}
+
+func testStoredBootFlags(t *testing.T, fields map[string]interface{}) map[string]json.RawMessage {
+	t.Helper()
+	js, err := json.Marshal(fields)
+	if err != nil {
+		t.Fatalf("marshal stored fields: %v", err)
+	}
+	stored := map[string]json.RawMessage{}
+	if err := json.Unmarshal(js, &stored); err != nil {
+		t.Fatalf("unmarshal stored fields: %v", err)
+	}
+	return stored
+}
+
+func TestMergeBootFlags(t *testing.T) {
+	fullStored := map[string]interface{}{
+		"Hostname":          "Saved Host",
+		"MaxUploadSize":     1024,
+		"MaxFileUploadBuff": 50,
+		"FileIOBuffer":      4096,
+		"DisableIPResolver": true,
+		"EnableHomePage":    false,
+		"EnableDirListing":  false,
+	}
+
+	tests := []struct {
+		name     string
+		runtime  bootFlags
+		stored   map[string]interface{}
+		explicit map[string]bool
+		want     bootFlags
+	}{
+		{
+			name:    "nothing stored keeps runtime defaults",
+			runtime: testDefaultBootFlags(),
+			stored:  map[string]interface{}{},
+			want:    testDefaultBootFlags(),
+		},
+		{
+			name:    "stored values restored when no start flag given",
+			runtime: testDefaultBootFlags(),
+			stored:  fullStored,
+			want: bootFlags{
+				Hostname:          "Saved Host",
+				MaxUploadSize:     1024,
+				MaxFileUploadBuff: 50,
+				FileIOBuffer:      4096,
+				DisableIPResolver: true,
+				EnableHomePage:    false,
+				EnableDirListing:  false,
+			},
+		},
+		{
+			name: "explicit start flags win over stored values",
+			runtime: bootFlags{
+				Hostname:          "Flag Host",
+				MaxUploadSize:     8192,
+				MaxFileUploadBuff: 25,
+				FileIOBuffer:      1024,
+				EnableHomePage:    true,
+				EnableDirListing:  true,
+			},
+			stored:   fullStored,
+			explicit: map[string]bool{"hostname": true, "homepage": true},
+			want: bootFlags{
+				Hostname:          "Flag Host",
+				MaxUploadSize:     1024,
+				MaxFileUploadBuff: 50,
+				FileIOBuffer:      4096,
+				DisableIPResolver: true,
+				EnableHomePage:    true,
+				EnableDirListing:  false,
+			},
+		},
+		{
+			name:    "partial stored config only restores its fields",
+			runtime: testDefaultBootFlags(),
+			stored:  map[string]interface{}{"MaxUploadSize": 2048},
+			want: func() bootFlags {
+				c := testDefaultBootFlags()
+				c.MaxUploadSize = 2048
+				return c
+			}(),
+		},
+		{
+			name:    "invalid stored values fall back to runtime",
+			runtime: testDefaultBootFlags(),
+			stored: map[string]interface{}{
+				"Hostname":          "  ",
+				"MaxUploadSize":     0,
+				"MaxFileUploadBuff": -1,
+				"FileIOBuffer":      "not a number",
+			},
+			want: testDefaultBootFlags(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := mergeBootFlags(tt.runtime, testStoredBootFlags(t, tt.stored), tt.explicit)
+			if got != tt.want {
+				t.Errorf("mergeBootFlags() = %+v, want %+v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestValidateBootFlags(t *testing.T) {
+	tests := []struct {
+		name    string
+		mutate  func(c *bootFlags)
+		wantErr bool
+	}{
+		{name: "defaults are valid", mutate: func(c *bootFlags) {}},
+		{name: "host name is trimmed", mutate: func(c *bootFlags) { c.Hostname = "  Box  " }},
+		{name: "empty host name", mutate: func(c *bootFlags) { c.Hostname = "   " }, wantErr: true},
+		{name: "zero upload size", mutate: func(c *bootFlags) { c.MaxUploadSize = 0 }, wantErr: true},
+		{name: "negative upload buffer", mutate: func(c *bootFlags) { c.MaxFileUploadBuff = -5 }, wantErr: true},
+		{name: "zero io buffer", mutate: func(c *bootFlags) { c.FileIOBuffer = 0 }, wantErr: true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := testDefaultBootFlags()
+			tt.mutate(&c)
+			err := validateBootFlags(&c)
+			if (err != nil) != tt.wantErr {
+				t.Fatalf("validateBootFlags() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if err == nil && c.Hostname != "My ArOZ" && c.Hostname != "Box" {
+				t.Errorf("unexpected host name after validation: %q", c.Hostname)
+			}
+		})
+	}
+}
+
+func TestBootFlagNamesCoverFlags(t *testing.T) {
+	for field, name := range bootFlagNames {
+		if flag.Lookup(name) == nil {
+			t.Errorf("field %s maps to unknown start flag -%s", field, name)
+		}
+	}
+	if len(bootFlagNames) != 7 {
+		t.Errorf("expected 7 mapped fields, got %d", len(bootFlagNames))
+	}
+}

+ 1 - 0
src/startup.go

@@ -97,6 +97,7 @@ func RunStartup() {
 		panic(err)
 	}
 	sysdb = dbconn
+	StartupFlagsRestore() //Restore saved startup parameters before any service reads them, see startup.flags.go
 
 	//2. Initiate the auth Agent
 	AuthInit() //See auth.go

+ 673 - 143
src/web/SystemAO/boot/bootflags.html

@@ -1,171 +1,701 @@
 <!DOCTYPE html>
 <html>
-    <head>
-        <title>Runtime Adjustment</title>
-        <meta charset="UTF-8">
-        <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
-        <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
-        <script type="text/javascript" src="../../script/jquery.min.js"></script>
-        <script type="text/javascript" src="../../script/semantic/semantic.min.js"></script>
-        <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
-        <script type="text/javascript" src="../../script/applocale.js"></script>
-        <style>
-            #bootflags_main_content{
-                padding: 20px;
-            }
-        </style>
-    </head>
-    <body>
-        <div id="bootflags_main_content" class="ui container">
-            <div class="ui basic segment">
-                <h3 class="ui header">
-                    <i class="play icon"></i>
-                    <div class="content">
-                        <span locale="runtime/runtime_adjustment">Runtime Adjustment</span>
-                        <div class="sub header" locale="runtime/update_system_startup_parameters">Update system startup parameters in runtime</div>
-                    </div>
-                </h3>
-                <div class="ui divider"></div>
-                <form id="setupform" class="ui form" onsubmit="handleFormSubmit(event, this);">
-                    <div class="field">
-                        <label locale="runtime/host_name">Host Name</label>
-                        <input class="config" type="text" name="Hostname" placeholder="My ArOZ">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
+    <title>Runtime</title>
+    <script src="../../script/jquery.min.js"></script>
+    <script src="../../script/ao_module.js"></script>
+    <script src="../../script/applocale.js"></script>
+    <style>
+/*
+   Runtime (startup parameters)
+   Scoped entirely to #rt-root to avoid conflicts with the parent
+   system-settings CSS. Follows the look of the Host Info page.
+*/
+
+#rt-root {
+    --rt-text:      #1d1d1f;
+    --rt-dim:       #6e6e73;
+    --rt-muted:     #98989d;
+    --rt-divider:   #e0e0e3;
+    --rt-card:      #ffffff;
+    --rt-card-bd:   #e5e5e7;
+    --rt-field:     #ffffff;
+    --rt-field-bd:  #d2d2d7;
+    --rt-soft:      rgba(0, 0, 0, 0.035);
+    --rt-accent:    #0071e3;
+    --rt-accent-bg: rgba(0, 113, 227, 0.10);
+    --rt-toggle:    #d1d1d6;
+    --rt-ok:        #1f9d55;
+    --rt-ok-bg:     rgba(31, 157, 85, 0.10);
+    --rt-err:       #d93025;
+    --rt-err-bg:    rgba(217, 48, 37, 0.09);
+    --rt-shadow:    rgba(0, 0, 0, 0.13);
+
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+    font-size: 14px;
+    -webkit-font-smoothing: antialiased;
+    color: var(--rt-text);
+    padding: 20px;
+    max-width: 760px;
+    margin: 0 auto;
+    box-sizing: border-box;
+}
+
+#rt-root.dark {
+    --rt-text:      #f0f0f0;
+    --rt-dim:       #a0a0a5;
+    --rt-muted:     #737378;
+    --rt-divider:   #3a3a3a;
+    --rt-card:      #2b2b2b;
+    --rt-card-bd:   #3b3b3b;
+    --rt-field:     #1f1f1f;
+    --rt-field-bd:  #48484a;
+    --rt-soft:      rgba(255, 255, 255, 0.05);
+    --rt-accent:    #2997ff;
+    --rt-accent-bg: rgba(41, 151, 255, 0.14);
+    --rt-toggle:    #48484a;
+    --rt-ok:        #4cc47f;
+    --rt-ok-bg:     rgba(76, 196, 127, 0.13);
+    --rt-err:       #ff6b61;
+    --rt-err-bg:    rgba(255, 107, 97, 0.13);
+    --rt-shadow:    rgba(0, 0, 0, 0.45);
+}
+
+#rt-root *, #rt-root *::before, #rt-root *::after { box-sizing: border-box; }
+
+/* Header */
+
+#rt-head {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 8px 0 22px;
+}
+
+#rt-head-icon {
+    width: 56px;
+    height: 56px;
+    border-radius: 14px;
+    flex-shrink: 0;
+    object-fit: cover;
+}
+
+#rt-title {
+    font-size: 21px;
+    font-weight: 600;
+    letter-spacing: -0.3px;
+    color: var(--rt-text) !important;
+    margin-bottom: 3px;
+}
+
+#rt-subtitle {
+    font-size: 13px;
+    color: var(--rt-dim) !important;
+    line-height: 1.45;
+}
+
+/* Groups */
+
+.rt-group { margin-bottom: 22px; }
+
+.rt-group-title {
+    font-size: 12px;
+    font-weight: 600;
+    letter-spacing: 0.2px;
+    text-transform: uppercase;
+    color: var(--rt-muted) !important;
+    margin: 0 0 8px 4px;
+}
+
+.rt-card {
+    background: var(--rt-card);
+    border: 1px solid var(--rt-card-bd);
+    border-radius: 12px;
+    overflow: hidden;
+}
+
+.rt-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 18px;
+    padding: 13px 16px;
+    border-bottom: 1px solid var(--rt-divider);
+}
+
+.rt-row:last-child { border-bottom: none; }
+
+.rt-row-text { min-width: 0; flex: 1; }
+
+.rt-row-label {
+    font-size: 13.5px;
+    font-weight: 500;
+    color: var(--rt-text) !important;
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 8px;
+}
+
+.rt-row-desc {
+    font-size: 12px;
+    color: var(--rt-dim) !important;
+    margin-top: 3px;
+    line-height: 1.45;
+}
+
+.rt-flag {
+    display: none;
+    font-size: 11px;
+    font-weight: 500;
+    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+    color: var(--rt-accent) !important;
+    background: var(--rt-accent-bg);
+    padding: 1px 7px;
+    border-radius: 20px;
+    white-space: nowrap;
+    cursor: help;
+}
+
+.rt-flag.show { display: inline-block; }
+
+/* Controls */
+
+.rt-control {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+#rt-root input[type="text"],
+#rt-root input[type="number"] {
+    font-family: inherit;
+    font-size: 13.5px;
+    color: var(--rt-text);
+    background: var(--rt-field);
+    border: 1px solid var(--rt-field-bd);
+    border-radius: 8px;
+    padding: 7px 10px;
+    outline: none;
+    transition: border-color 0.12s, box-shadow 0.12s;
+}
+
+#rt-root input[type="text"]   { width: 240px; }
+#rt-root input[type="number"] { width: 120px; text-align: right; }
+
+#rt-root input[type="text"]:focus,
+#rt-root input[type="number"]:focus {
+    border-color: var(--rt-accent);
+    box-shadow: 0 0 0 3px var(--rt-accent-bg);
+}
+
+#rt-root input.rt-invalid {
+    border-color: var(--rt-err);
+    box-shadow: 0 0 0 3px var(--rt-err-bg);
+}
+
+.rt-unit {
+    font-size: 12px;
+    color: var(--rt-dim) !important;
+    min-width: 34px;
+}
+
+/* Toggle switch */
+
+.rt-switch {
+    position: relative;
+    display: inline-block;
+    width: 42px;
+    height: 24px;
+    flex-shrink: 0;
+    cursor: pointer;
+}
+
+.rt-switch input {
+    position: absolute;
+    opacity: 0;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    cursor: pointer;
+    z-index: 1;
+}
+
+.rt-switch .rt-track {
+    position: absolute;
+    inset: 0;
+    border-radius: 24px;
+    background: var(--rt-toggle);
+    transition: background 0.18s;
+}
+
+.rt-switch .rt-track::after {
+    content: "";
+    position: absolute;
+    top: 2px;
+    left: 2px;
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    background: #ffffff;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+    transition: transform 0.18s;
+}
+
+.rt-switch input:checked + .rt-track { background: var(--rt-accent); }
+.rt-switch input:checked + .rt-track::after { transform: translateX(18px); }
+.rt-switch input:focus-visible + .rt-track { box-shadow: 0 0 0 3px var(--rt-accent-bg); }
+
+/* Info note */
+
+.rt-note {
+    display: flex;
+    gap: 10px;
+    align-items: flex-start;
+    padding: 12px 14px;
+    border-radius: 10px;
+    background: var(--rt-soft);
+    font-size: 12.5px;
+    line-height: 1.5;
+    color: var(--rt-dim) !important;
+    margin-bottom: 18px;
+}
+
+.rt-note svg {
+    width: 16px;
+    height: 16px;
+    flex-shrink: 0;
+    margin-top: 1px;
+    color: var(--rt-accent);
+}
+
+.rt-note code {
+    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+    font-size: 11.5px;
+}
+
+/* Action bar */
+
+#rt-actions {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    gap: 10px;
+    flex-wrap: wrap;
+    padding-top: 16px;
+    border-top: 1px solid var(--rt-divider);
+}
+
+#rt-status {
+    margin-right: auto;
+    font-size: 12.5px;
+    padding: 5px 10px;
+    border-radius: 7px;
+    display: none;
+    align-items: center;
+    gap: 6px;
+}
+
+#rt-status.ok  { display: inline-flex; color: var(--rt-ok) !important;  background: var(--rt-ok-bg); }
+#rt-status.err { display: inline-flex; color: var(--rt-err) !important; background: var(--rt-err-bg); }
+#rt-status svg { width: 14px; height: 14px; flex-shrink: 0; }
+
+.rt-btn {
+    font-family: inherit;
+    font-size: 13.5px;
+    padding: 8px 18px;
+    border-radius: 8px;
+    border: none;
+    cursor: pointer;
+    transition: opacity 0.12s, background 0.12s;
+}
+
+.rt-btn:disabled { opacity: 0.45; cursor: default; }
+
+.rt-btn-primary { background: var(--rt-accent); color: #ffffff !important; }
+.rt-btn-primary:not(:disabled):hover { opacity: 0.88; }
+
+.rt-btn-plain {
+    background: transparent;
+    color: var(--rt-accent) !important;
+}
+.rt-btn-plain:not(:disabled):hover { background: var(--rt-accent-bg); }
+
+/* Responsive */
+
+@media (max-width: 560px) {
+    #rt-root { padding: 14px; }
+    .rt-row {
+        flex-direction: column;
+        align-items: stretch;
+        gap: 10px;
+    }
+    .rt-control { justify-content: flex-start; }
+    .rt-row.rt-row-toggle {
+        flex-direction: row;
+        align-items: center;
+    }
+    #rt-root input[type="text"] { width: 100%; }
+    .rt-control-text { width: 100%; }
+    #rt-status { width: 100%; margin-right: 0; }
+}
+    </style>
+</head>
+<body>
+
+<div id="rt-root">
+
+    <div id="rt-head">
+        <img id="rt-head-icon" src="../info/img/runtime.png" alt="">
+        <div>
+            <div id="rt-title" locale="runtime/title">Runtime</div>
+            <div id="rt-subtitle" locale="runtime/subtitle">Adjust startup parameters while the system is running. Changes are saved and restored on every boot.</div>
+        </div>
+    </div>
+
+    <form id="rt-form" autocomplete="off" onsubmit="return false;">
+
+        <div class="rt-group">
+            <div class="rt-group-title" locale="runtime/group_general">General</div>
+            <div class="rt-card">
+                <div class="rt-row">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/host_name">Host Name</span>
+                            <span class="rt-flag" data-for="Hostname">-hostname</span>
+                        </div>
+                        <div class="rt-row-desc" locale="runtime/host_name_note">Name shown to users and on the local network. Network discovery picks up a new name after a restart.</div>
                     </div>
-                    <div class="field">
-                        <label locale="runtime/max_upload_size">Max Upload Size (MB)</label>
-                        <input class="config" type="number" name="MaxUploadSize" placeholder="8192">
+                    <div class="rt-control rt-control-text">
+                        <input class="rt-config" type="text" name="Hostname" placeholder="My ArOZ" maxlength="64">
                     </div>
-                    <div class="field">
-                        <label locale="runtime/file_upload_buffer">File Upload Buffer (MB)</label>
-                        <input class="config" type="number" name="MaxFileUploadBuff" placeholder="25">
+                </div>
+            </div>
+        </div>
+
+        <div class="rt-group">
+            <div class="rt-group-title" locale="runtime/group_upload">Upload &amp; File I/O</div>
+            <div class="rt-card">
+                <div class="rt-row">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/max_upload_size">Max Upload Size</span>
+                            <span class="rt-flag" data-for="MaxUploadSize">-max_upload_size</span>
+                        </div>
+                        <div class="rt-row-desc" locale="runtime/max_upload_size_note">Largest file a user can upload. Must not exceed the memory available on this host.</div>
                     </div>
-                    <div class="field">
-                        <label locale="runtime/file_io_buffer">File IO Buffer (Bytes, Default 1024)</label>
-                        <input class="config" type="number" name="FileIOBuffer" placeholder="1024">
+                    <div class="rt-control">
+                        <input class="rt-config" type="number" name="MaxUploadSize" min="1" step="1" placeholder="8192">
+                        <span class="rt-unit">MB</span>
                     </div>
-                    <div class="field">
-                        <div class="ui toggle checkbox">
-                            <input class="config" type="checkbox" name="DisableIPResolver" tabindex="0">
-                            <label locale="runtime/disable_ip_resolver">Disable IP Resolver</label>
-                            <small locale="runtime/disable_ip_resolver_note">Not trying to analyze Client IP address, useful when the server is hosted under NAT or Reverse Proxy</small>
+                </div>
+                <div class="rt-row">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/file_upload_buffer">File Upload Buffer</span>
+                            <span class="rt-flag" data-for="MaxFileUploadBuff">-upload_buf</span>
                         </div>
+                        <div class="rt-row-desc" locale="runtime/file_upload_buffer_note">Uploads larger than this are buffered to disk instead of memory (slower).</div>
                     </div>
-                    <div class="field">
-                        <div class="ui toggle checkbox">
-                            <input class="config" type="checkbox" name="EnableHomePage" tabindex="0">
-                            <label locale="runtime/enable_home_page">Enable Home Page</label>
-                            <small locale="runtime/enable_home_page_note">Allowing users to host their own static web pages</small>
-                        </div>
+                    <div class="rt-control">
+                        <input class="rt-config" type="number" name="MaxFileUploadBuff" min="1" step="1" placeholder="25">
+                        <span class="rt-unit">MB</span>
                     </div>
-                    <div class="field">
-                        <div class="ui toggle checkbox">
-                            <input class="config" type="checkbox" name="EnableDirListing" tabindex="0">
-                            <label locale="runtime/enable_directory_listing">Enable Directory Listing</label>
-                            <small locale="runtime/enable_directory_listing_note">Allow listing the contents of a directory if the request URL is a folder</small>
+                </div>
+                <div class="rt-row">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/file_io_buffer">File IO Buffer</span>
+                            <span class="rt-flag" data-for="FileIOBuffer">-iobuf</span>
                         </div>
+                        <div class="rt-row-desc" locale="runtime/file_io_buffer_note">Memory used for each file operation. Default is 1024.</div>
                     </div>
-                    <div class="ui divider"></div>
-                    <p locale="runtime/confirmation_message">To continue updating your boot configuration, please confirm that: </p>
-                    <div class="field">
-                        <div class="ui checkbox">
-                            <input type="checkbox" tabindex="0" onchange="toggleConfirmBtn(this.checked)">
-                            <label locale="runtime/i_know_what_i_am_doing">I know exactly what I am doing</label>
-                            <small locale="runtime/i_know_what_i_am_doing_note">These changes are only applied to the current runtime session. If something goes wrong, you can restart your system to restore it to the default value.</small>
+                    <div class="rt-control">
+                        <input class="rt-config" type="number" name="FileIOBuffer" min="1" step="1" placeholder="1024">
+                        <span class="rt-unit" locale="runtime/unit_bytes">Bytes</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="rt-group">
+            <div class="rt-group-title" locale="runtime/group_web">Web Services</div>
+            <div class="rt-card">
+                <div class="rt-row rt-row-toggle">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/disable_ip_resolver">Disable IP Resolver</span>
+                            <span class="rt-flag" data-for="DisableIPResolver">-disable_ip_resolver</span>
                         </div>
+                        <div class="rt-row-desc" locale="runtime/disable_ip_resolver_note">Do not try to analyze the client IP address. Useful when the server is hosted behind NAT or a reverse proxy.</div>
                     </div>
-                    <button id="confirmbtn" class="ui red disabled button" type="submit" locale="runtime/write_to_runtime">Write to Runtime</button>
-                </form>
-                <div class="ui red message" style="display:none;" id="failedmsg" locale="runtime/update_failed">
-                    <div class="header">
-                        <i class="remove icon"></i> Flags Update Failed
+                    <label class="rt-switch">
+                        <input class="rt-config" type="checkbox" name="DisableIPResolver">
+                        <span class="rt-track"></span>
+                    </label>
+                </div>
+                <div class="rt-row rt-row-toggle">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/enable_home_page">Enable Home Page</span>
+                            <span class="rt-flag" data-for="EnableHomePage">-homepage</span>
+                        </div>
+                        <div class="rt-row-desc" locale="runtime/enable_home_page_note">Allow users to host their own static web pages.</div>
                     </div>
-                    <ul class="list">
-                        <p id="errmsg">Your settings were not applied due to an unknown error.</p>
-                    </ul>
+                    <label class="rt-switch">
+                        <input class="rt-config" type="checkbox" name="EnableHomePage">
+                        <span class="rt-track"></span>
+                    </label>
                 </div>
-                <div id="succmsg" class="ui green message transition" style="display:none;" locale="runtime/param_updated">
-                    <div class="header">
-                        <i class="checkmark icon"></i> Startup parameter Updated
+                <div class="rt-row rt-row-toggle">
+                    <div class="rt-row-text">
+                        <div class="rt-row-label">
+                            <span locale="runtime/enable_directory_listing">Enable Directory Listing</span>
+                            <span class="rt-flag" data-for="EnableDirListing">-dir_list</span>
+                        </div>
+                        <div class="rt-row-desc" locale="runtime/enable_directory_listing_note">Allow listing the contents of a directory if the request URL is a folder.</div>
                     </div>
-                    <ul class="list">
-                        <p locale="runtime/param_updated_note">These settings will only be available in the current runtime session. Restart your server to restore the original settings.</p>
-                    </ul>
+                    <label class="rt-switch">
+                        <input class="rt-config" type="checkbox" name="EnableDirListing">
+                        <span class="rt-track"></span>
+                    </label>
                 </div>
             </div>
         </div>
-        
-            <br><br>
+
+        <div class="rt-note">
+            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
+                <circle cx="8" cy="8" r="6.5"/>
+                <path d="M8 7.2v4" stroke-linecap="round"/>
+                <circle cx="8" cy="4.9" r="0.6" fill="currentColor" stroke="none"/>
+            </svg>
+            <div locale="runtime/priority_note">Saved values are restored when the system starts. A value passed as a start parameter (for example <code>-hostname</code>) takes priority: it replaces the saved value on that boot. Settings marked with their flag name were set this way.</div>
+        </div>
+
+        <div id="rt-actions">
+            <span id="rt-status"></span>
+            <button type="button" class="rt-btn rt-btn-plain" id="rt-revert" disabled locale="runtime/revert">Revert</button>
+            <button type="button" class="rt-btn rt-btn-primary" id="rt-save" disabled locale="runtime/save">Save</button>
         </div>
-        <script>
-
-            var runtimeLocale = NewAppLocale();
-            runtimeLocale.init("../locale/system_settings/runtime.json", function(){
-                runtimeLocale.translate();
-                initCurrentSettingList();
-            });
-
-            function handleFormSubmit(event, obj){
-                event.preventDefault();
-                var result = {};
-                $(obj).find(".config").each(function(){
-                    var name = $(this).attr("name");
-                    var value = $(this).val();
-                    if ($(this).attr('type') == "checkbox"){
-                        value =  $(this)[0].checked
-                    }else if ($(this).attr('type') == "number"){
-                        value = parseInt(value);
-                    }
-                    result[name] = value;
+    </form>
+
+</div><!-- #rt-root -->
+
+<script>
+(function () {
+    /*
+        Own locale instance rather than the shared global one: this page is
+        loaded as a fragment into System Settings, which keeps its own strings
+        on the global applocale.
+    */
+    var rtLocale = (typeof NewAppLocale === "function") ? NewAppLocale() : {
+        init: function (f, cb) { cb(); },
+        translate: function () {},
+        getString: function (k, fallback) { return fallback; }
+    };
+
+    function rtStr(key, fallback) {
+        try {
+            return rtLocale.getString(key, fallback) || fallback;
+        } catch (e) {
+            return fallback;
+        }
+    }
+
+    var ICON_OK = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg>';
+    var ICON_ERR = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="8" cy="8" r="6.5"/><path d="M8 4.8v4"/><path d="M8 11.2v0.1"/></svg>';
+
+    var root = document.getElementById("rt-root");
+    var loaded = null;      // last values read from the server
+    var statusTimer = null;
+
+    /* Theme */
+    function rtApplyTheme() {
+        try {
+            var theme = (typeof preferredTheme !== "undefined") ? preferredTheme :
+                        (parent && parent.preferredTheme) ? parent.preferredTheme : undefined;
+            if (theme && theme !== "auto") {
+                root.classList.toggle("dark", theme === "dark");
+            } else if (typeof ao_module_getSystemThemeColor === "function") {
+                ao_module_getSystemThemeColor(function (c) {
+                    root.classList.toggle("dark", c !== "whiteTheme");
                 });
+            }
+        } catch (e) {}
+    }
 
-                //Send the results back to the backend
-                console.log(result);
-                $.ajax({
-                    url: "../../system/bootflags",
-                    data: {opr: "set", value: JSON.stringify(result)},
-                    method: "POST",
-                    success: function(data){
-                        if (data.error != undefined){
-                            $("#errmsg").text(data.error);
-                            $("#failedmsg").slideDown("fast").delay(10000).slideUp('fast');
-                        }else{
-                            $("#succmsg").slideDown("fast").delay(3000).slideUp('fast');
-                        }
-
-                        //Update the list again
-                        initCurrentSettingList();
-                    }
-                })
+    window.detailPageThemeCallback = function (isDark) {
+        var el = document.getElementById("rt-root");
+        if (el) el.classList.toggle("dark", isDark);
+    };
+
+    /* Form helpers */
+    function fields() {
+        return root.querySelectorAll(".rt-config");
+    }
+
+    function readForm() {
+        var result = {};
+        fields().forEach(function (input) {
+            if (input.type === "checkbox") {
+                result[input.name] = input.checked;
+            } else if (input.type === "number") {
+                result[input.name] = parseInt(input.value, 10);
+            } else {
+                result[input.name] = input.value.trim();
             }
+        });
+        return result;
+    }
 
-            function initCurrentSettingList(){
-                $.get("../../system/bootflags", function(data){
-                    if (data.errpr !== undefined){
-                        $("#errmsg").text(data.error);
-                        $("#failedmsg").slideDown("fast").delay(10000).slideUp('fast');
-                    }else{
-                        //Load the data
-                        for (var [key, value] of Object.entries(data)) {
-                            var targetInputField = $(`[name ='${key}']`);
-                            if (targetInputField.attr("type") == "text"){
-                                targetInputField.val(value);
-                            }else if (targetInputField.attr("type") == "checkbox"){
-                                targetInputField[0].checked = value;
-                            }else if (targetInputField.attr("type") == "number"){
-                                targetInputField.val(value);
-                            }
-                        }
-                    }
-                });
+    function fillForm(data) {
+        fields().forEach(function (input) {
+            if (!(input.name in data)) return;
+            if (input.type === "checkbox") {
+                input.checked = !!data[input.name];
+            } else {
+                input.value = data[input.name];
+            }
+            input.classList.remove("rt-invalid");
+        });
+
+        var fromFlag = data.FromStartFlag || [];
+        root.querySelectorAll(".rt-flag").forEach(function (badge) {
+            var isSet = fromFlag.indexOf(badge.getAttribute("data-for")) >= 0;
+            badge.classList.toggle("show", isSet);
+            badge.title = rtStr("runtime/flag_tooltip", "Set by a start parameter this boot. It will replace the saved value again on the next boot unless the parameter is removed.");
+        });
+    }
+
+    function isDirty() {
+        if (loaded === null) return false;
+        var current = readForm();
+        for (var key in current) {
+            if (current[key] !== loaded[key]) return true;
+        }
+        return false;
+    }
+
+    function validate() {
+        var ok = true;
+        fields().forEach(function (input) {
+            var bad = false;
+            if (input.type === "number") {
+                var v = parseInt(input.value, 10);
+                bad = !(v > 0);
+            } else if (input.type === "text") {
+                bad = input.value.trim() === "";
+            }
+            input.classList.toggle("rt-invalid", bad);
+            if (bad) ok = false;
+        });
+        return ok;
+    }
+
+    function refreshButtons() {
+        var dirty = isDirty();
+        document.getElementById("rt-save").disabled = !dirty;
+        document.getElementById("rt-revert").disabled = !dirty;
+    }
+
+    function showStatus(ok, text) {
+        var el = document.getElementById("rt-status");
+        clearTimeout(statusTimer);
+        el.className = ok ? "ok" : "err";
+        el.innerHTML = (ok ? ICON_OK : ICON_ERR);
+        var span = document.createElement("span");
+        span.textContent = text;
+        el.appendChild(span);
+        if (ok) {
+            statusTimer = setTimeout(function () { el.className = ""; }, 3500);
+        }
+    }
+
+    /* Server I/O */
+    function rtLoad() {
+        $.get("../../system/bootflags", function (data) {
+            if (data == null || data.error !== undefined) {
+                showStatus(false, (data && data.error) || rtStr("runtime/load_failed", "Unable to load the current settings"));
+                return;
+            }
+            loaded = {};
+            for (var key in data) {
+                if (key !== "FromStartFlag") loaded[key] = data[key];
             }
+            fillForm(data);
+            refreshButtons();
+        }).fail(function () {
+            showStatus(false, rtStr("runtime/load_failed", "Unable to load the current settings"));
+        });
+    }
 
-            function toggleConfirmBtn(value){
-                if (value == true){
-                    $("#confirmbtn").removeClass("disabled");
-                }else{
-                    $("#confirmbtn").addClass("disabled");
+    function rtSave() {
+        if (!validate()) {
+            showStatus(false, rtStr("runtime/invalid", "Check the highlighted fields: values must be larger than zero and the host name cannot be empty."));
+            return;
+        }
+
+        var btn = document.getElementById("rt-save");
+        btn.disabled = true;
+        $.ajax({
+            url: "../../system/bootflags",
+            method: "POST",
+            data: { opr: "set", value: JSON.stringify(readForm()) },
+            success: function (data) {
+                if (data != null && data.error !== undefined) {
+                    showStatus(false, data.error);
+                    refreshButtons();
+                    return;
                 }
+                showStatus(true, rtStr("runtime/saved", "Saved. The new values are active and will be restored on the next boot."));
+                rtLoad();
+            },
+            error: function () {
+                showStatus(false, rtStr("runtime/save_failed", "Could not save the settings"));
+                refreshButtons();
             }
-        </script>
-    </body>
-</html>
+        });
+    }
+
+    /* Boot */
+    rtApplyTheme();
+
+    fields().forEach(function (input) {
+        input.addEventListener("input", function () {
+            input.classList.remove("rt-invalid");
+            refreshButtons();
+        });
+        input.addEventListener("change", refreshButtons);
+    });
+
+    document.getElementById("rt-save").addEventListener("click", rtSave);
+    document.getElementById("rt-revert").addEventListener("click", function () {
+        if (loaded) fillForm($.extend({}, loaded, { FromStartFlag: currentFlagFields() }));
+        refreshButtons();
+    });
+    document.getElementById("rt-form").addEventListener("keydown", function (e) {
+        if (e.key === "Enter" && e.target.tagName === "INPUT" && !document.getElementById("rt-save").disabled) {
+            e.preventDefault();
+            rtSave();
+        }
+    });
+
+    function currentFlagFields() {
+        var list = [];
+        root.querySelectorAll(".rt-flag.show").forEach(function (b) {
+            list.push(b.getAttribute("data-for"));
+        });
+        return list;
+    }
+
+    rtLocale.init("../locale/system_settings/runtime.json", function () {
+        rtLocale.translate();
+        rtLoad();
+    });
+})();
+</script>
+</body>
+</html>

+ 157 - 103
src/web/SystemAO/locale/system_settings/runtime.json

@@ -6,25 +6,34 @@
             "name": "繁體中文(台灣)",
             "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
             "strings": {
-                "runtime/runtime_adjustment": "即時執行環境調整",
-                "runtime/update_system_startup_parameters": "在運行時更新系統啟動參數",
+                "runtime/title": "執行環境",
+                "runtime/subtitle": "在系統運行時調整啟動參數。變更會被儲存,並在每次啟動時還原。",
+                "runtime/group_general": "一般",
+                "runtime/group_upload": "上傳與檔案 I/O",
+                "runtime/group_web": "網頁服務",
                 "runtime/host_name": "伺服器名稱",
-                "runtime/max_upload_size": "最大上傳大小(MB)",
-                "runtime/file_upload_buffer": "文件上傳緩衝區(MB)",
-                "runtime/file_io_buffer": "文件 IO 緩衝區(位元組,預設 1024)",
-                "runtime/disable_ip_resolver": "禁用 IP 解析器",
-                "runtime/disable_ip_resolver_note": "不嘗試分析客戶端 IP 地址,在服務器位於 NAT 或反向代理下時有用",
-                "runtime/enable_home_page": "啟用首頁",
-                "runtime/enable_home_page_note": "允許用戶託管自己的靜態網頁",
+                "runtime/host_name_note": "向使用者及區域網路顯示的名稱。網路探索服務會在重新啟動後使用新名稱。",
+                "runtime/max_upload_size": "最大上傳大小",
+                "runtime/max_upload_size_note": "使用者可上傳的最大檔案。不應超過此主機的可用記憶體。",
+                "runtime/file_upload_buffer": "檔案上傳緩衝區",
+                "runtime/file_upload_buffer_note": "超過此大小的上傳會緩衝至磁碟而非記憶體(較慢)。",
+                "runtime/file_io_buffer": "檔案 IO 緩衝區",
+                "runtime/file_io_buffer_note": "每個檔案操作使用的記憶體,預設為 1024。",
+                "runtime/unit_bytes": "位元組",
+                "runtime/disable_ip_resolver": "停用 IP 解析器",
+                "runtime/disable_ip_resolver_note": "不嘗試分析用戶端 IP 位址,適用於伺服器位於 NAT 或反向代理之後的情況。",
+                "runtime/enable_home_page": "啟用個人首頁",
+                "runtime/enable_home_page_note": "允許使用者託管自己的靜態網頁。",
                 "runtime/enable_directory_listing": "啟用目錄列表",
-                "runtime/enable_directory_listing_note": "如果請求 URL 是資料夾,則允許列出目錄的內容",
-                "runtime/confirmation_message": "繼續更新您的啟動配置,請確認:",
-                "runtime/i_know_what_i_am_doing": "我確定我知道在做什麼",
-                "runtime/i_know_what_i_am_doing_note": "這些設置僅會應用在當前的執行環境。如果出現問題,您可以重新啟動系統以恢復到預設值。",
-                "runtime/write_to_runtime": "寫入當前執行環境",
-                "runtime/update_failed": "參數更新失敗",
-                "runtime/param_updated": "啟動參數已更新",
-                "runtime/param_updated_note": "這些設置僅會應用在當前的執行環境。重新啟動您的服務器以恢復到預設值。",
+                "runtime/enable_directory_listing_note": "若請求的 URL 是資料夾,允許列出其內容。",
+                "runtime/priority_note": "已儲存的數值會在系統啟動時還原。以啟動參數傳入的數值(例如 <code>-hostname</code>)優先:該次啟動時會取代已儲存的數值。標示了參數名稱的項目即是以此方式設定。",
+                "runtime/flag_tooltip": "本次啟動由啟動參數設定。除非移除該參數,下次啟動時仍會取代已儲存的數值。",
+                "runtime/revert": "還原",
+                "runtime/save": "儲存",
+                "runtime/saved": "已儲存。新數值已生效,並會在下次啟動時還原。",
+                "runtime/save_failed": "無法儲存設定",
+                "runtime/load_failed": "無法載入目前的設定",
+                "runtime/invalid": "請檢查標示的欄位:數值必須大於零,且伺服器名稱不可為空。",
                 "": ""
             },
             "titles": {},
@@ -34,25 +43,34 @@
             "name": "繁體中文(香港)",
             "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
             "strings": {
-                "runtime/runtime_adjustment": "即時執行環境調整",
-                "runtime/update_system_startup_parameters": "在運行時更新系統啟動參數",
+                "runtime/title": "執行環境",
+                "runtime/subtitle": "在系統運行時調整啟動參數。變更會被儲存,並在每次啟動時還原。",
+                "runtime/group_general": "一般",
+                "runtime/group_upload": "上傳與檔案 I/O",
+                "runtime/group_web": "網頁服務",
                 "runtime/host_name": "伺服器名稱",
-                "runtime/max_upload_size": "最大上傳大小(MB)",
-                "runtime/file_upload_buffer": "文件上傳緩衝區(MB)",
-                "runtime/file_io_buffer": "文件 IO 緩衝區(位元組,預設 1024)",
-                "runtime/disable_ip_resolver": "禁用 IP 解析器",
-                "runtime/disable_ip_resolver_note": "不嘗試分析客戶端 IP 地址,在服務器位於 NAT 或反向代理下時有用",
-                "runtime/enable_home_page": "啟用首頁",
-                "runtime/enable_home_page_note": "允許用戶託管自己的靜態網頁",
+                "runtime/host_name_note": "向使用者及區域網路顯示的名稱。網路探索服務會在重新啟動後使用新名稱。",
+                "runtime/max_upload_size": "最大上傳大小",
+                "runtime/max_upload_size_note": "使用者可上傳的最大檔案。不應超過此主機的可用記憶體。",
+                "runtime/file_upload_buffer": "檔案上傳緩衝區",
+                "runtime/file_upload_buffer_note": "超過此大小的上傳會緩衝至磁碟而非記憶體(較慢)。",
+                "runtime/file_io_buffer": "檔案 IO 緩衝區",
+                "runtime/file_io_buffer_note": "每個檔案操作使用的記憶體,預設為 1024。",
+                "runtime/unit_bytes": "位元組",
+                "runtime/disable_ip_resolver": "停用 IP 解析器",
+                "runtime/disable_ip_resolver_note": "不嘗試分析用戶端 IP 位址,適用於伺服器位於 NAT 或反向代理之後的情況。",
+                "runtime/enable_home_page": "啟用個人首頁",
+                "runtime/enable_home_page_note": "允許使用者託管自己的靜態網頁。",
                 "runtime/enable_directory_listing": "啟用目錄列表",
-                "runtime/enable_directory_listing_note": "如果請求 URL 是資料夾,則允許列出目錄的內容",
-                "runtime/confirmation_message": "繼續更新您的啟動配置,請確認:",
-                "runtime/i_know_what_i_am_doing": "我確定我知道在做什麼",
-                "runtime/i_know_what_i_am_doing_note": "這些設置僅會應用在當前的執行環境。如果出現問題,您可以重新啟動系統以恢復到預設值。",
-                "runtime/write_to_runtime": "寫入當前執行環境",
-                "runtime/update_failed": "參數更新失敗",
-                "runtime/param_updated": "啟動參數已更新",
-                "runtime/param_updated_note": "這些設置僅會應用在當前的執行環境。重新啟動您的服務器以恢復到預設值。",
+                "runtime/enable_directory_listing_note": "若請求的 URL 是資料夾,允許列出其內容。",
+                "runtime/priority_note": "已儲存的數值會在系統啟動時還原。以啟動參數傳入的數值(例如 <code>-hostname</code>)優先:該次啟動時會取代已儲存的數值。標示了參數名稱的項目即是以此方式設定。",
+                "runtime/flag_tooltip": "本次啟動由啟動參數設定。除非移除該參數,下次啟動時仍會取代已儲存的數值。",
+                "runtime/revert": "還原",
+                "runtime/save": "儲存",
+                "runtime/saved": "已儲存。新數值已生效,並會在下次啟動時還原。",
+                "runtime/save_failed": "無法儲存設定",
+                "runtime/load_failed": "無法載入目前的設定",
+                "runtime/invalid": "請檢查標示的欄位:數值必須大於零,且伺服器名稱不可為空。",
                 "": ""
             },
             "titles": {},
@@ -62,25 +80,34 @@
             "name": "简体中文(简体)",
             "fwtitle": "",
             "strings": {
-                "runtime/runtime_adjustment": "即时执行环境调整",
-                "runtime/update_system_startup_parameters": "在运行时更新系统启动参数",
-                "runtime/host_name": "伺服器名称",
-                "runtime/max_upload_size": "最大上传大小(MB)",
-                "runtime/file_upload_buffer": "文件上传缓冲区(MB)",
-                "runtime/file_io_buffer": "文件 IO 缓冲区(位元组,预设 1024)",
+                "runtime/title": "运行环境",
+                "runtime/subtitle": "在系统运行时调整启动参数。更改会被保存,并在每次启动时恢复。",
+                "runtime/group_general": "常规",
+                "runtime/group_upload": "上传与文件 I/O",
+                "runtime/group_web": "网页服务",
+                "runtime/host_name": "服务器名称",
+                "runtime/host_name_note": "向用户及局域网显示的名称。网络发现服务会在重启后使用新名称。",
+                "runtime/max_upload_size": "最大上传大小",
+                "runtime/max_upload_size_note": "用户可上传的最大文件。不应超过此主机的可用内存。",
+                "runtime/file_upload_buffer": "文件上传缓冲区",
+                "runtime/file_upload_buffer_note": "超过此大小的上传会缓冲到磁盘而非内存(较慢)。",
+                "runtime/file_io_buffer": "文件 IO 缓冲区",
+                "runtime/file_io_buffer_note": "每个文件操作使用的内存,默认为 1024。",
+                "runtime/unit_bytes": "字节",
                 "runtime/disable_ip_resolver": "禁用 IP 解析器",
-                "runtime/disable_ip_resolver_note": "不尝试分析客户端 IP 地址,在服务器位于 NAT 或反向代理下时有用",
-                "runtime/enable_home_page": "启用首页",
-                "runtime/enable_home_page_note": "允许用户托管自己的静态网页",
+                "runtime/disable_ip_resolver_note": "不尝试分析客户端 IP 地址,适用于服务器位于 NAT 或反向代理之后的情况。",
+                "runtime/enable_home_page": "启用个人主页",
+                "runtime/enable_home_page_note": "允许用户托管自己的静态网页。",
                 "runtime/enable_directory_listing": "启用目录列表",
-                "runtime/enable_directory_listing_note": "如果请求 URL 是资料夹,则允许列出目录的内容",
-                "runtime/confirmation_message": "继续更新您的启动配置,请确认:",
-                "runtime/i_know_what_i_am_doing": "我确定我知道在做什么",
-                "runtime/i_know_what_i_am_doing_note": "这些设置仅会应用在当前的执行环境。如果出现问题,您可以重新启动系统以恢复到预设值。",
-                "runtime/write_to_runtime": "写入当前执行环境",
-                "runtime/update_failed": "参数更新失败",
-                "runtime/param_updated": "启动参数已更新",
-                "runtime/param_updated_note": "这些设置仅会应用在当前的执行环境。重新启动您的服务器以恢复到预设值。",
+                "runtime/enable_directory_listing_note": "若请求的 URL 是文件夹,允许列出其内容。",
+                "runtime/priority_note": "已保存的值会在系统启动时恢复。以启动参数传入的值(例如 <code>-hostname</code>)优先:该次启动时会替换已保存的值。标有参数名称的项目即是以此方式设置。",
+                "runtime/flag_tooltip": "本次启动由启动参数设置。除非移除该参数,下次启动时仍会替换已保存的值。",
+                "runtime/revert": "还原",
+                "runtime/save": "保存",
+                "runtime/saved": "已保存。新值已生效,并会在下次启动时恢复。",
+                "runtime/save_failed": "无法保存设置",
+                "runtime/load_failed": "无法加载当前设置",
+                "runtime/invalid": "请检查标示的字段:数值必须大于零,且服务器名称不能为空。",
                 "": ""
             },
             "titles": {},
@@ -90,25 +117,34 @@
             "name": "English (US)",
             "fontFamily": "Arial, Helvetica, sans-serif",
             "strings": {
-                "runtime/runtime_adjustment": "Runtime Adjustment",
-                "runtime/update_system_startup_parameters": "Update system startup parameters at runtime",
-                "runtime/host_name": "Server Name",
-                "runtime/max_upload_size": "Max Upload Size (MB)",
-                "runtime/file_upload_buffer": "File Upload Buffer (MB)",
-                "runtime/file_io_buffer": "File IO Buffer (bytes, default 1024)",
+                "runtime/title": "Runtime",
+                "runtime/subtitle": "Adjust startup parameters while the system is running. Changes are saved and restored on every boot.",
+                "runtime/group_general": "General",
+                "runtime/group_upload": "Upload &amp; File I/O",
+                "runtime/group_web": "Web Services",
+                "runtime/host_name": "Host Name",
+                "runtime/host_name_note": "Name shown to users and on the local network. Network discovery picks up a new name after a restart.",
+                "runtime/max_upload_size": "Max Upload Size",
+                "runtime/max_upload_size_note": "Largest file a user can upload. Must not exceed the memory available on this host.",
+                "runtime/file_upload_buffer": "File Upload Buffer",
+                "runtime/file_upload_buffer_note": "Uploads larger than this are buffered to disk instead of memory (slower).",
+                "runtime/file_io_buffer": "File IO Buffer",
+                "runtime/file_io_buffer_note": "Memory used for each file operation. Default is 1024.",
+                "runtime/unit_bytes": "Bytes",
                 "runtime/disable_ip_resolver": "Disable IP Resolver",
-                "runtime/disable_ip_resolver_note": "Do not attempt to resolve client IP addresses; useful when the server is behind NAT or a reverse proxy",
-                "runtime/enable_home_page": "Enable Homepage",
-                "runtime/enable_home_page_note": "Allow users to host their own static web pages",
+                "runtime/disable_ip_resolver_note": "Do not try to analyze the client IP address. Useful when the server is hosted behind NAT or a reverse proxy.",
+                "runtime/enable_home_page": "Enable Home Page",
+                "runtime/enable_home_page_note": "Allow users to host their own static web pages.",
                 "runtime/enable_directory_listing": "Enable Directory Listing",
-                "runtime/enable_directory_listing_note": "Allow listing directory contents if the requested URL is a folder",
-                "runtime/confirmation_message": "To proceed with updating your startup configuration, please confirm:",
-                "runtime/i_know_what_i_am_doing": "I confirm I know what I am doing",
-                "runtime/i_know_what_i_am_doing_note": "These settings will only be applied to the current runtime environment. If problems occur, you can restart the system to restore defaults.",
-                "runtime/write_to_runtime": "Write to Current Runtime",
-                "runtime/update_failed": "Parameter update failed",
-                "runtime/param_updated": "Startup parameters updated",
-                "runtime/param_updated_note": "These settings will only be applied to the current runtime environment. Restart your server to restore defaults.",
+                "runtime/enable_directory_listing_note": "Allow listing the contents of a directory if the request URL is a folder.",
+                "runtime/priority_note": "Saved values are restored when the system starts. A value passed as a start parameter (for example <code>-hostname</code>) takes priority: it replaces the saved value on that boot. Settings marked with their flag name were set this way.",
+                "runtime/flag_tooltip": "Set by a start parameter this boot. It will replace the saved value again on the next boot unless the parameter is removed.",
+                "runtime/revert": "Revert",
+                "runtime/save": "Save",
+                "runtime/saved": "Saved. The new values are active and will be restored on the next boot.",
+                "runtime/save_failed": "Could not save the settings",
+                "runtime/load_failed": "Unable to load the current settings",
+                "runtime/invalid": "Check the highlighted fields: values must be larger than zero and the host name cannot be empty.",
                 "": ""
             },
             "titles": {},
@@ -118,25 +154,34 @@
             "name": "日本語",
             "fontFamily": "\"Meiryo UI\", \"Arial Unicode MS\", \"Hiragino Kaku Gothic Pro\"",
             "strings": {
-                "runtime/runtime_adjustment": "ランタイム調整",
-                "runtime/update_system_startup_parameters": "ランタイム中にシステム起動パラメータを更新",
+                "runtime/title": "ランタイム",
+                "runtime/subtitle": "システムの実行中に起動パラメータを調整します。変更は保存され、起動のたびに復元されます。",
+                "runtime/group_general": "一般",
+                "runtime/group_upload": "アップロードとファイル I/O",
+                "runtime/group_web": "Web サービス",
                 "runtime/host_name": "サーバー名",
-                "runtime/max_upload_size": "最大アップロードサイズ(MB)",
-                "runtime/file_upload_buffer": "ファイルアップロードバッファ(MB)",
-                "runtime/file_io_buffer": "ファイルIOバッファ(バイト、デフォルト1024)",
-                "runtime/disable_ip_resolver": "IPリゾルバーを無効化",
-                "runtime/disable_ip_resolver_note": "クライアントIPアドレスの解決を試みません。サーバーがNATまたはリバースプロキシの後ろにある場合に有用です",
+                "runtime/host_name_note": "ユーザーとローカルネットワークに表示される名前です。ネットワーク検出は再起動後に新しい名前を使用します。",
+                "runtime/max_upload_size": "最大アップロードサイズ",
+                "runtime/max_upload_size_note": "ユーザーがアップロードできる最大ファイルサイズです。このホストの利用可能なメモリを超えないでください。",
+                "runtime/file_upload_buffer": "ファイルアップロードバッファ",
+                "runtime/file_upload_buffer_note": "これより大きいアップロードはメモリではなくディスクにバッファされます(低速)。",
+                "runtime/file_io_buffer": "ファイル IO バッファ",
+                "runtime/file_io_buffer_note": "各ファイル操作で使用するメモリです。デフォルトは 1024 です。",
+                "runtime/unit_bytes": "バイト",
+                "runtime/disable_ip_resolver": "IP リゾルバーを無効化",
+                "runtime/disable_ip_resolver_note": "クライアントの IP アドレスを解析しません。サーバーが NAT やリバースプロキシの背後にある場合に有用です。",
                 "runtime/enable_home_page": "ホームページを有効化",
-                "runtime/enable_home_page_note": "ユーザーが自分の静的ウェブページをホストできるようにします",
-                "runtime/enable_directory_listing": "ディレクトリリストを有効化",
-                "runtime/enable_directory_listing_note": "リクエストされたURLがフォルダの場合、ディレクトリの内容のリストを許可します",
-                "runtime/confirmation_message": "起動設定の更新を続行するには確認してください:",
-                "runtime/i_know_what_i_am_doing": "何をしているか分かっています",
-                "runtime/i_know_what_i_am_doing_note": "これらの設定は現在のランタイム環境にのみ適用されます。問題が発生した場合はシステムを再起動してデフォルト値に戻すことができます。",
-                "runtime/write_to_runtime": "現在のランタイムに書き込む",
-                "runtime/update_failed": "パラメータ更新失敗",
-                "runtime/param_updated": "起動パラメータが更新されました",
-                "runtime/param_updated_note": "これらの設定は現在のランタイム環境にのみ適用されます。デフォルト値に戻すにはサーバーを再起動してください。",
+                "runtime/enable_home_page_note": "ユーザーが自分の静的 Web ページをホストできるようにします。",
+                "runtime/enable_directory_listing": "ディレクトリ一覧を有効化",
+                "runtime/enable_directory_listing_note": "リクエスト URL がフォルダの場合、その内容の一覧表示を許可します。",
+                "runtime/priority_note": "保存された値はシステム起動時に復元されます。起動パラメータで渡された値(例:<code>-hostname</code>)が優先され、その起動時に保存値を置き換えます。フラグ名が表示されている項目はこの方法で設定されています。",
+                "runtime/flag_tooltip": "今回の起動では起動パラメータで設定されています。パラメータを削除しない限り、次回の起動時にも保存値を置き換えます。",
+                "runtime/revert": "元に戻す",
+                "runtime/save": "保存",
+                "runtime/saved": "保存しました。新しい値は有効になり、次回の起動時に復元されます。",
+                "runtime/save_failed": "設定を保存できませんでした",
+                "runtime/load_failed": "現在の設定を読み込めませんでした",
+                "runtime/invalid": "強調表示された項目を確認してください。数値は 0 より大きく、サーバー名は空にできません。",
                 "": ""
             },
             "titles": {},
@@ -146,29 +191,38 @@
             "name": "한국어",
             "fontFamily": "\"Malgun Gothic\", \"Apple SD Gothic Neo\"",
             "strings": {
-                "runtime/runtime_adjustment": "런타임 조정",
-                "runtime/update_system_startup_parameters": "런타임 중 시스템 시작 매개변수 업데이트",
+                "runtime/title": "런타임",
+                "runtime/subtitle": "시스템 실행 중에 시작 매개변수를 조정합니다. 변경 사항은 저장되어 부팅할 때마다 복원됩니다.",
+                "runtime/group_general": "일반",
+                "runtime/group_upload": "업로드 및 파일 I/O",
+                "runtime/group_web": "웹 서비스",
                 "runtime/host_name": "서버 이름",
-                "runtime/max_upload_size": "최대 업로드 크기 (MB)",
-                "runtime/file_upload_buffer": "파일 업로드 버퍼 (MB)",
-                "runtime/file_io_buffer": "파일 IO 버퍼 (바이트, 기본값 1024)",
-                "runtime/disable_ip_resolver": "IP 리졸버 비활성화",
-                "runtime/disable_ip_resolver_note": "클라이언트 IP 주소 분석을 시도하지 않습니다. 서버가 NAT 또는 리버스 프록시 뒤에 있을 때 유용합니다",
+                "runtime/host_name_note": "사용자와 로컬 네트워크에 표시되는 이름입니다. 네트워크 검색은 재시작 후 새 이름을 사용합니다.",
+                "runtime/max_upload_size": "최대 업로드 크기",
+                "runtime/max_upload_size_note": "사용자가 업로드할 수 있는 최대 파일 크기입니다. 이 호스트의 사용 가능한 메모리를 넘지 않아야 합니다.",
+                "runtime/file_upload_buffer": "파일 업로드 버퍼",
+                "runtime/file_upload_buffer_note": "이보다 큰 업로드는 메모리 대신 디스크에 버퍼링됩니다(느림).",
+                "runtime/file_io_buffer": "파일 IO 버퍼",
+                "runtime/file_io_buffer_note": "각 파일 작업에 사용되는 메모리입니다. 기본값은 1024입니다.",
+                "runtime/unit_bytes": "바이트",
+                "runtime/disable_ip_resolver": "IP 확인기 비활성화",
+                "runtime/disable_ip_resolver_note": "클라이언트 IP 주소를 분석하지 않습니다. 서버가 NAT 또는 리버스 프록시 뒤에 있을 때 유용합니다.",
                 "runtime/enable_home_page": "홈페이지 활성화",
-                "runtime/enable_home_page_note": "사용자가 자신의 정적 웹 페이지를 호스팅할 수 있도록 허용합니다",
-                "runtime/enable_directory_listing": "디렉토리 목록 활성화",
-                "runtime/enable_directory_listing_note": "요청된 URL이 폴더인 경우 디렉토리 내용 목록을 허용합니다",
-                "runtime/confirmation_message": "시작 설정 업데이트를 진행하려면 확인하세요:",
-                "runtime/i_know_what_i_am_doing": "무엇을 하고 있는지 알고 있습니다",
-                "runtime/i_know_what_i_am_doing_note": "이 설정은 현재 런타임 환경에만 적용됩니다. 문제가 발생하면 시스템을 재시작하여 기본값으로 복원할 수 있습니다.",
-                "runtime/write_to_runtime": "현재 런타임에 쓰기",
-                "runtime/update_failed": "매개변수 업데이트 실패",
-                "runtime/param_updated": "시작 매개변수가 업데이트되었습니다",
-                "runtime/param_updated_note": "이 설정은 현재 런타임 환경에만 적용됩니다. 기본값으로 복원하려면 서버를 재시작하세요.",
+                "runtime/enable_home_page_note": "사용자가 자신의 정적 웹 페이지를 호스팅할 수 있도록 허용합니다.",
+                "runtime/enable_directory_listing": "디렉터리 목록 활성화",
+                "runtime/enable_directory_listing_note": "요청 URL이 폴더인 경우 그 내용을 나열하도록 허용합니다.",
+                "runtime/priority_note": "저장된 값은 시스템 시작 시 복원됩니다. 시작 매개변수로 전달된 값(예: <code>-hostname</code>)이 우선하며 해당 부팅에서 저장된 값을 대체합니다. 플래그 이름이 표시된 항목은 이 방식으로 설정된 것입니다.",
+                "runtime/flag_tooltip": "이번 부팅에서 시작 매개변수로 설정되었습니다. 매개변수를 제거하지 않으면 다음 부팅에서도 저장된 값을 대체합니다.",
+                "runtime/revert": "되돌리기",
+                "runtime/save": "저장",
+                "runtime/saved": "저장되었습니다. 새 값이 적용되었으며 다음 부팅 시 복원됩니다.",
+                "runtime/save_failed": "설정을 저장할 수 없습니다",
+                "runtime/load_failed": "현재 설정을 불러올 수 없습니다",
+                "runtime/invalid": "강조 표시된 항목을 확인하세요. 값은 0보다 커야 하며 서버 이름은 비워 둘 수 없습니다.",
                 "": ""
             },
             "titles": {},
             "placeholder": {}
         }
     }
-}
+}