From b88d648e14faafa685f0e06bd06edeed395119af Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:32:36 +0000 Subject: [PATCH 1/3] WiFi: dump link/regdomain/wpa log on failed connect, wait 45s Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- menus/wifi.sh | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/menus/wifi.sh b/menus/wifi.sh index f412d92..fcce4d0 100644 --- a/menus/wifi.sh +++ b/menus/wifi.sh @@ -142,6 +142,21 @@ wifi_choose_iface() { fi } +# What the adapter is actually doing, printed after a failed connect so the +# cause (regulatory domain, weak signal, DHCP, driver) is visible, not guessed. +wifi_debug_dump() { + local i="$1" + echo "--- link ---" + iw dev "$i" link 2>&1 | head -8 + echo "--- regulatory domain ---" + iw reg get 2>&1 | grep -m2 "country\|global" || true + echo "--- wpa_supplicant log (last 15) ---" + journalctl -b -u "netplan-wpa-$i" -n 15 --no-pager -o cat 2>&1 | cut -c1-160 + echo "--- networkd ---" + networkctl status "$i" --no-pager 2>&1 | head -12 + echo "(full log: /var/lib/kiosk-firstboot/install.log, or journalctl -b -u 'netplan-wpa-*')" +} + wifi_menu() { local rc=0 wifi_configure || rc=$? @@ -407,9 +422,9 @@ WATCHEOF netplan_log=$(mktemp) if sudo netplan apply > "$netplan_log" 2>&1; then cat "$netplan_log" - echo "Waiting for $wifi_iface to connect (up to 30 seconds)..." + echo "Waiting for $wifi_iface to connect (up to 45 seconds)..." local _ - for _ in {1..30}; do + for _ in {1..45}; do wifi_iface_has_ip "$wifi_iface" && break sleep 1 done @@ -422,11 +437,12 @@ WATCHEOF why=$(wifi_diagnose "$wifi_iface") if [[ -n "$why" ]]; then log_error "Couldn't connect to $ssid: $why" + elif iw dev "$wifi_iface" link 2>/dev/null | grep -q "^Connected"; then + log_error "Joined the network but got no address - the router's DHCP isn't answering (or is slow)" else - echo "Wrong password or network name is the usual cause." + echo "Not joined, and the log doesn't say why - not necessarily the password." fi - echo "Details:" - echo " sudo journalctl -b -u 'netplan-wpa-*' -u systemd-networkd -n 30 --no-pager" + wifi_debug_dump "$wifi_iface" fi else log_error "netplan apply failed" From 9221fc85aaf3e6c0c1c147a34fe2591b69c32ca5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:42:44 +0000 Subject: [PATCH 2/3] WiFi: set regulatory domain (country) in the WiFi wizard and web helper Without a country Linux uses the restrictive world domain: networks show up in scans but the adapter can't join them. The menu asks (default from the existing config or system locale); the web helper uses the same default. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- menus/wifi.sh | 35 +++++++++++++++++-- .../files/usr/local/bin/kiosk-wifi-helper | 24 ++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/menus/wifi.sh b/menus/wifi.sh index fcce4d0..b73d174 100644 --- a/menus/wifi.sh +++ b/menus/wifi.sh @@ -157,6 +157,24 @@ wifi_debug_dump() { echo "(full log: /var/lib/kiosk-firstboot/install.log, or journalctl -b -u 'netplan-wpa-*')" } +# Two-letter country code for the WiFi regulatory domain: the one already +# in our netplan file, else from the system locale (en_US.UTF-8 -> US). +# Without a country set, Linux uses the restrictive "world" domain: many +# 5 GHz channels become listen-only, so a network shows up in scans but +# the adapter can't join it. +wifi_default_country() { + local c="" + if [[ -f "$WIFI_NETPLAN_FILE" ]]; then + c=$(sed -n 's/^[[:space:]]*regulatory-domain:[[:space:]]*["'"'"']\{0,1\}\([A-Za-z][A-Za-z]\).*/\1/p' "$WIFI_NETPLAN_FILE" | head -1) + fi + if [[ -z "$c" ]]; then + local loc="${LANG:-}" + [[ -r /etc/default/locale ]] && loc=$(sed -n 's/^LANG=["'"'"']\{0,1\}\([^"'"'"']*\).*/\1/p' /etc/default/locale | head -1) + [[ "$loc" =~ _([A-Z][A-Z])[.@]? ]] && c="${BASH_REMATCH[1]}" + fi + echo "${c^^}" +} + wifi_menu() { local rc=0 wifi_configure || rc=$? @@ -351,7 +369,17 @@ wifi_configure() { return 1 fi - apply_wifi_config "$wifi_iface" "$ssid" "$password" + local country c_in + country=$(wifi_default_country) + read -r -p "Country code for WiFi regulations (2 letters, e.g. US) [${country:-none}]: " c_in + c_in=${c_in:-$country} + c_in=${c_in^^} + if [[ -n "$c_in" && ! "$c_in" =~ ^[A-Z]{2}$ ]]; then + log_warning "'$c_in' isn't a 2-letter country code - skipping it" + c_in="" + fi + + apply_wifi_config "$wifi_iface" "$ssid" "$password" "$c_in" } # apply_wifi_config IFACE SSID PASSWORD @@ -361,6 +389,7 @@ apply_wifi_config() { local wifi_iface="$1" local ssid="$2" local password="$3" + local country="${4:-}" # Our own file, merged by netplan with the others (wired config etc). local netplan_file="$WIFI_NETPLAN_FILE" @@ -384,7 +413,8 @@ network: dhcp4: true dhcp6: false optional: true - access-points: +${country:+ regulatory-domain: \"$country\" +} access-points: $(wifi_yaml_quote "$ssid"): password: $(wifi_yaml_quote "$password") EOF @@ -417,6 +447,7 @@ WATCHEOF rm -f "$temp_plan" sudo bash -c "$(declare -f wifi_drop_stale_entries); NETPLAN_DIR='$NETPLAN_DIR' WIFI_NETPLAN_FILE='$netplan_file' wifi_drop_stale_entries" + [[ -n "$country" ]] && sudo iw reg set "$country" 2>/dev/null echo "Applying configuration..." local netplan_log netplan_log=$(mktemp) diff --git a/provision/files/usr/local/bin/kiosk-wifi-helper b/provision/files/usr/local/bin/kiosk-wifi-helper index 9099c68..dd66c19 100755 --- a/provision/files/usr/local/bin/kiosk-wifi-helper +++ b/provision/files/usr/local/bin/kiosk-wifi-helper @@ -139,6 +139,25 @@ except Exception: sys.exit(1)' "$WIFI_NETPLAN_FILE" "$SYS_CLASS" 2>/dev/null } + +# Two-letter country code for the WiFi regulatory domain: the one already +# in our netplan file, else from the system locale (en_US.UTF-8 -> US). +# Without a country set, Linux uses the restrictive "world" domain: many +# 5 GHz channels become listen-only, so a network shows up in scans but +# the adapter can't join it. +default_country() { + local c="" + if [[ -f "$WIFI_NETPLAN_FILE" ]]; then + c=$(sed -n 's/^[[:space:]]*regulatory-domain:[[:space:]]*["'"'"']\{0,1\}\([A-Za-z][A-Za-z]\).*/\1/p' "$WIFI_NETPLAN_FILE" | head -1) + fi + if [[ -z "$c" ]]; then + local loc="${LANG:-}" + [[ -r /etc/default/locale ]] && loc=$(sed -n 's/^LANG=["'"'"']\{0,1\}\([^"'"'"']*\).*/\1/p' /etc/default/locale | head -1) + [[ "$loc" =~ _([A-Z][A-Z])[.@]? ]] && c="${BASH_REMATCH[1]}" + fi + echo "${c^^}" +} + need_iface() { IFACE=$(prefer_configured_iface || detect_iface) || reply_error "No WiFi adapter found. If it's a USB adapter, check it's plugged in." command -v iw >/dev/null || reply_error "WiFi support (iw, wpasupplicant) isn't installed - run Core Settings -> WiFi once from the admin menu" @@ -264,7 +283,9 @@ do_connect() { unblock hard_blocked && reply_error "WiFi is switched off by a hardware switch or in the BIOS/UEFI" - local backup="" new + local country backup="" new + country=$(default_country) + [[ -n "$country" ]] && iw reg set "$country" 2>/dev/null mkdir -p "$NETPLAN_DIR" if [[ -f "$WIFI_NETPLAN_FILE" ]]; then backup="${WIFI_NETPLAN_FILE}.backup-$(date +%Y%m%d-%H%M%S)" @@ -281,6 +302,7 @@ do_connect() { echo " dhcp4: true" echo " dhcp6: false" echo " optional: true" + [[ -n "$country" ]] && echo " regulatory-domain: \"$country\"" echo " access-points:" if [[ -n "$password" ]]; then echo " $(yaml_quote "$ssid"):" From d0d39e48bb95595d5f1176350822d9672d00e12d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:48:41 +0000 Subject: [PATCH 3/3] Region setting (WiFi country, locale, keyboard) on the Super+W screen and Web UI kiosk-wifi-helper gains region-status/region-set. The country is persisted as a cfg80211 regdom module option and in the WiFi netplan file; locale and keyboard go through localectl, validated against what's installed. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- kiosk-app/main.js | 7 ++ kiosk-app/wifi.html | 69 +++++++++++++- menus/wifi.sh | 3 + .../files/usr/local/bin/kiosk-wifi-helper | 92 ++++++++++++++++++- webui/public/app.js | 58 ++++++++++++ webui/public/index.html | 33 +++++++ webui/server.js | 35 +++++++ webui/test/api.test.js | 28 ++++++ webui/test/fixtures/fake-wifi-helper.sh | 8 ++ 9 files changed, 331 insertions(+), 2 deletions(-) create mode 100755 webui/test/fixtures/fake-wifi-helper.sh diff --git a/kiosk-app/main.js b/kiosk-app/main.js index 30dd880..14fd168 100644 --- a/kiosk-app/main.js +++ b/kiosk-app/main.js @@ -1328,6 +1328,13 @@ ipcMain.handle('wifi-request',async(event,req)=>{ } if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'}; if(action==='status'||action==='scan')return runWifiHelper(action); + if(action==='region-status')return runWifiHelper('region-status'); + if(action==='region-set'){ + const input={}; + for(const k of['country','locale','keyboard'])if(req[k])input[k]=String(req[k]); + console.log('[WIFI] Region set:',JSON.stringify(input)); + return runWifiHelper('region-set',input); + } if(action==='connect'){ console.log('[WIFI] Connecting to',JSON.stringify(String(req.ssid||''))); const result=await runWifiHelper('connect',{ssid:String(req.ssid||''),password:String(req.password||'')}); diff --git a/kiosk-app/wifi.html b/kiosk-app/wifi.html index a4ae1fe..dbb6277 100644 --- a/kiosk-app/wifi.html +++ b/kiosk-app/wifi.html @@ -26,6 +26,10 @@ color: #ecf0f1; outline: none; } + select { + width: 100%; padding: 14px; font-size: 18px; border: 2px solid #34495e; + border-radius: 10px; background: #34495e; color: #ecf0f1; + } input:focus { border-color: #3498db; } .row { display: flex; gap: 10px; align-items: center; } .row input { flex: 1; } @@ -114,6 +118,20 @@ + +
Esc closes this screen. Closes by itself after 5 minutes untouched.
@@ -135,7 +153,7 @@ function setBusy(on) { busy = on; - for (const id of ['scan', 'connect', 'pin-ok']) $(id).disabled = on; + for (const id of ['scan', 'connect', 'pin-ok', 'region-save']) $(id).disabled = on; } function signalBars(dbm) { @@ -210,6 +228,52 @@ refreshStatus(); } + function fillSelect(sel, items, current) { + sel.textContent = ''; + const list = items.slice(); + if (current && !list.includes(current)) list.unshift(current); + for (const v of list) { + const o = document.createElement('option'); + o.value = v; o.textContent = v; + if (v === current) o.selected = true; + sel.appendChild(o); + } + } + + async function loadRegion() { + const r = await request('region-status'); + if (!r.ok) return; + $('country').value = r.country || ''; + fillSelect($('locale'), r.locales || [], r.locale); + fillSelect($('keyboard'), r.keyboards || [], r.keyboard); + $('region-panel').classList.remove('hidden'); + } + + async function saveRegion() { + if (busy) return; + setBusy(true); + message('Saving region…', 'busy'); + const r = await request('region-set', { + country: $('country').value.trim(), + locale: $('locale').value, + keyboard: $('keyboard').value, + }); + setBusy(false); + if (r.ok) { + message(r.restart ? '✓ Saved. Locale and keyboard apply after a restart; the country applies now.' : '✓ Saved', 'ok'); + loadRegion(); + } else { + message(r.error || 'Could not save', 'error'); + } + } + + // en_US.UTF-8 -> US + function countryFromLocale() { + const m = /_([A-Z]{2})/.exec($('locale').value); + if (m) $('country').value = m[1]; + else message("That locale doesn't name a country", 'error'); + } + async function unlock() { if (busy) return; setBusy(true); @@ -224,12 +288,15 @@ $('pin-step').classList.add('hidden'); $('wifi-step').classList.remove('hidden'); refreshStatus(); + loadRegion(); scan(); } $('pin-ok').addEventListener('click', unlock); $('pin').addEventListener('keydown', e => { if (e.key === 'Enter') unlock(); }); $('scan').addEventListener('click', scan); + $('region-save').addEventListener('click', saveRegion); + $('region-from-locale').addEventListener('click', countryFromLocale); $('connect').addEventListener('click', connect); $('password').addEventListener('keydown', e => { if (e.key === 'Enter') connect(); }); $('ssid').addEventListener('keydown', e => { if (e.key === 'Enter') $('password').focus(); }); diff --git a/menus/wifi.sh b/menus/wifi.sh index b73d174..357dc5e 100644 --- a/menus/wifi.sh +++ b/menus/wifi.sh @@ -167,6 +167,9 @@ wifi_default_country() { if [[ -f "$WIFI_NETPLAN_FILE" ]]; then c=$(sed -n 's/^[[:space:]]*regulatory-domain:[[:space:]]*["'"'"']\{0,1\}\([A-Za-z][A-Za-z]\).*/\1/p' "$WIFI_NETPLAN_FILE" | head -1) fi + if [[ -z "$c" && -r /etc/modprobe.d/kiosk-regdom.conf ]]; then + c=$(sed -n 's/.*ieee80211_regdom=\([A-Za-z][A-Za-z]\).*/\1/p' /etc/modprobe.d/kiosk-regdom.conf | head -1) + fi if [[ -z "$c" ]]; then local loc="${LANG:-}" [[ -r /etc/default/locale ]] && loc=$(sed -n 's/^LANG=["'"'"']\{0,1\}\([^"'"'"']*\).*/\1/p' /etc/default/locale | head -1) diff --git a/provision/files/usr/local/bin/kiosk-wifi-helper b/provision/files/usr/local/bin/kiosk-wifi-helper index dd66c19..f1d7d10 100755 --- a/provision/files/usr/local/bin/kiosk-wifi-helper +++ b/provision/files/usr/local/bin/kiosk-wifi-helper @@ -11,6 +11,11 @@ # kiosk-wifi-helper status {"iface","connected","ssid","ip","configured"} # kiosk-wifi-helper scan {"iface","networks":[{"ssid","signal","secure"}]} # kiosk-wifi-helper connect stdin {"ssid","password"} -> {"ok","ip"|"error"} +# kiosk-wifi-helper region-status +# {"country","locale","keyboard","locales":[],"keyboards":[]} +# kiosk-wifi-helper region-set stdin {"country"?,"locale"?,"keyboard"?} -> {"ok",...} +# WiFi country (regulatory domain), system +# locale and keyboard layout; every field optional # # Every reply is one JSON line on stdout, exit 0 on success. connect # writes the same netplan file as Core Settings -> WiFi (menus/wifi.sh, @@ -24,6 +29,8 @@ set -uo pipefail NETPLAN_DIR="${NETPLAN_DIR:-/etc/netplan}" SYS_CLASS="${KIOSK_WIFI_SYS_CLASS:-/sys/class}" # overridable for tests WIFI_NETPLAN_FILE="$NETPLAN_DIR/60-kiosk-wifi.yaml" +REGDOM_CONF="${KIOSK_REGDOM_CONF:-/etc/modprobe.d/kiosk-regdom.conf}" +LOCALE_FILE="${KIOSK_LOCALE_FILE:-/etc/default/locale}" LOCK_FILE="${KIOSK_WIFI_LOCK:-/run/kiosk-wifi-helper.lock}" CONNECT_WAIT="${KIOSK_WIFI_CONNECT_WAIT:-30}" @@ -150,9 +157,12 @@ default_country() { if [[ -f "$WIFI_NETPLAN_FILE" ]]; then c=$(sed -n 's/^[[:space:]]*regulatory-domain:[[:space:]]*["'"'"']\{0,1\}\([A-Za-z][A-Za-z]\).*/\1/p' "$WIFI_NETPLAN_FILE" | head -1) fi + if [[ -z "$c" && -f "$REGDOM_CONF" ]]; then + c=$(sed -n 's/.*ieee80211_regdom=\([A-Za-z][A-Za-z]\).*/\1/p' "$REGDOM_CONF" | head -1) + fi if [[ -z "$c" ]]; then local loc="${LANG:-}" - [[ -r /etc/default/locale ]] && loc=$(sed -n 's/^LANG=["'"'"']\{0,1\}\([^"'"'"']*\).*/\1/p' /etc/default/locale | head -1) + [[ -r "$LOCALE_FILE" ]] && loc=$(sed -n 's/^LANG=["'"'"']\{0,1\}\([^"'"'"']*\).*/\1/p' "$LOCALE_FILE" | head -1) [[ "$loc" =~ _([A-Z][A-Z])[.@]? ]] && c="${BASH_REMATCH[1]}" fi echo "${c^^}" @@ -163,6 +173,84 @@ need_iface() { command -v iw >/dev/null || reply_error "WiFi support (iw, wpasupplicant) isn't installed - run Core Settings -> WiFi once from the admin menu" } +# ---- Region: WiFi country, system locale, keyboard layout ----------------- + +current_locale() { + [[ -r "$LOCALE_FILE" ]] || return 0 + sed -n 's/^LANG=["'"'"']\{0,1\}\([^"'"'"']*\).*/\1/p' "$LOCALE_FILE" | head -1 +} + +current_keyboard() { + localectl status 2>/dev/null | sed -n 's/^[[:space:]]*X11 Layout:[[:space:]]*//p' | head -1 +} + +# Locales that are generated on this system (setting any other one would +# leave the whole system on a locale that doesn't exist). +list_locales() { + locale -a 2>/dev/null | grep -i 'utf-\?8' | sed 's/\.[Uu][Tt][Ff]-\?8$/.UTF-8/' | sort -u +} + +list_keyboards() { + localectl list-x11-keymap-layouts 2>/dev/null +} + +do_region_status() { + jq -cn --arg c "$(default_country)" --arg l "$(current_locale)" --arg k "$(current_keyboard)" \ + --argjson ls "$(list_locales | jq -R . | jq -cs .)" \ + --argjson ks "$(list_keyboards | jq -R . | jq -cs .)" \ + '{ok:true,country:$c,locale:$l,keyboard:$k,locales:$ls,keyboards:$ks}' +} + +# Persist the WiFi country: kernel option for the regulatory domain (so it +# applies from boot, before any WiFi config exists), the netplan file if +# there is one (so wpa_supplicant gets it too), and now via iw. +set_country() { + local c="$1" + printf '# Written by kiosk-wifi-helper (Region setting)\noptions cfg80211 ieee80211_regdom=%s\n' "$c" > "$REGDOM_CONF" + if [[ -f "$WIFI_NETPLAN_FILE" ]]; then + sed -i '/^[[:space:]]*regulatory-domain:/d' "$WIFI_NETPLAN_FILE" + sed -i "s/^\([[:space:]]*\)optional: true\$/&\n\1regulatory-domain: \"$c\"/" "$WIFI_NETPLAN_FILE" + fi + iw reg set "$c" 2>/dev/null || true +} + +do_region_set() { + local input country locale keyboard + input=$(head -c 4096) + country=$(jq -r '.country // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request" + locale=$(jq -r '.locale // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request" + keyboard=$(jq -r '.keyboard // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request" + country=${country^^} + + if [[ -n "$country" ]]; then + [[ "$country" =~ ^[A-Z]{2}$ ]] || reply_error "A country code is 2 letters, e.g. US" + # Reject codes that aren't real countries when the tz database is here. + if [[ -r /usr/share/zoneinfo/iso3166.tab ]] && ! grep -q "^$country[[:space:]]" /usr/share/zoneinfo/iso3166.tab; then + reply_error "$country isn't a known country code" + fi + fi + if [[ -n "$locale" ]]; then + list_locales | grep -qxF -- "$locale" || reply_error "Locale $locale isn't installed on this system" + fi + if [[ -n "$keyboard" ]]; then + list_keyboards | grep -qxF -- "$keyboard" || reply_error "Unknown keyboard layout: $keyboard" + fi + + local err + [[ -n "$country" ]] && set_country "$country" + if [[ -n "$locale" ]]; then + err=$(localectl set-locale "LANG=$locale" 2>&1) || reply_error "Couldn't set the locale: ${err:0:200}" + fi + if [[ -n "$keyboard" ]]; then + err=$(localectl set-x11-keymap "$keyboard" 2>&1) || reply_error "Couldn't set the keyboard: ${err:0:200}" + # Console layout too, when there's a console map of the same name. + localectl list-keymaps 2>/dev/null | grep -qxF -- "$keyboard" && localectl set-keymap "$keyboard" >/dev/null 2>&1 + fi + jq -cn --arg c "$(default_country)" --arg l "$(current_locale)" --arg k "$(current_keyboard)" \ + --argjson restart "$([[ -n "$locale$keyboard" ]] && echo true || echo false)" \ + '{ok:true,country:$c,locale:$l,keyboard:$k,restart:$restart}' +} + do_status() { local iface ssid="" ip="" configured configured=$(configured_ssid) @@ -370,5 +458,7 @@ case "${1:-}" in status) do_status ;; scan) do_scan ;; connect) do_connect ;; + region-status) do_region_status ;; + region-set) do_region_set ;; *) reply_error "unknown action: ${1:-}" ;; esac diff --git a/webui/public/app.js b/webui/public/app.js index 0d6a939..0a3a947 100644 --- a/webui/public/app.js +++ b/webui/public/app.js @@ -643,6 +643,63 @@ document.getElementById('run-upgrade').addEventListener('click', async (e) => { } }); +/* ---------------------------------------------------------------------- */ +/* Region */ +/* ---------------------------------------------------------------------- */ + +function fillSelect(sel, items, current) { + sel.textContent = ''; + const list = items.slice(); + if (current && !list.includes(current)) list.unshift(current); + for (const v of list) { + const o = document.createElement('option'); + o.value = v; + o.textContent = v; + o.selected = v === current; + sel.appendChild(o); + } +} + +async function loadRegion() { + try { + const res = await fetch('/api/region'); + const r = await res.json(); + if (!res.ok) throw new Error(r.error || 'Could not read the region'); + document.getElementById('region-country').value = r.country || ''; + fillSelect(document.getElementById('region-locale'), r.locales || [], r.locale); + fillSelect(document.getElementById('region-keyboard'), r.keyboards || [], r.keyboard); + } catch (e) { + showMessage(e.message, true); + } +} + +document.getElementById('region-form').addEventListener('submit', async (ev) => { + ev.preventDefault(); + try { + const res = await fetch('/api/region', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + country: document.getElementById('region-country').value.trim(), + locale: document.getElementById('region-locale').value, + keyboard: document.getElementById('region-keyboard').value, + }), + }); + const r = await res.json(); + if (!res.ok) throw new Error(r.error || 'Could not save'); + showMessage(r.restart ? 'Region saved - restart for the locale and keyboard to apply' : 'Region saved'); + loadRegion(); + } catch (e) { + showMessage(e.message, true); + } +}); + +document.getElementById('region-from-locale').addEventListener('click', () => { + const m = /_([A-Z]{2})/.exec(document.getElementById('region-locale').value); + if (m) document.getElementById('region-country').value = m[1]; + else showMessage("That locale doesn't name a country", true); +}); + /* ---------------------------------------------------------------------- */ /* Init */ /* ---------------------------------------------------------------------- */ @@ -657,6 +714,7 @@ async function init() { } await loadAddonStatus(); renderAddons(); + loadRegion(); } init(); diff --git a/webui/public/index.html b/webui/public/index.html index d898f4a..313452a 100644 --- a/webui/public/index.html +++ b/webui/public/index.html @@ -36,6 +36,10 @@ Addons + + + +

Locale and keyboard apply after a restart; the WiFi country applies immediately.

+ + + +