From 3cdb943a21839d58c0d9e215a1b9097947020a86 Mon Sep 17 00:00:00 2001 From: action Date: Wed, 4 Nov 2026 23:55:01 +0800 Subject: [PATCH] update 2026-11-04 23:55:01 --- honk/Makefile | 12 +- honk/files/control.sh | 260 +++++++++++++++++- honk/files/lifecycle.sh | 16 +- luci-app-honk/Makefile | 4 +- .../resources/view/honk/configuration.js | 4 + .../resources/view/honk/overview.js | 82 +++++- .../luci-static/resources/view/honk/rpc.js | 8 + luci-app-honk/po/zh-cn/honk.po | 114 ++++++-- luci-app-honk/root/usr/libexec/rpcd/honk | 11 +- .../usr/share/rpcd/acl.d/luci-app-honk.json | 3 +- luci-app-ssr-plus/Makefile | 2 +- luci-theme-aurora/.dev/src/media/login.css | 4 + .../.dev/tests/build-performance.test.js | 3 +- luci-theme-aurora/Makefile | 2 +- .../htdocs/luci-static/aurora/login.css | 2 +- .../ucode/template/themes/aurora/sysauth.ut | 44 ++- .../htdocs/luci-static/resources/fs-fit.js | 25 +- .../htdocs/luci-static/resources/fs-router.js | 18 ++ .../styles/theme/70-modal.css | 2 + luci-theme-shadcn/.dev/src/media/login.css | 13 +- luci-theme-shadcn/Makefile | 2 +- .../htdocs/luci-static/shadcn/login.css | 2 +- .../ucode/template/themes/shadcn/header.ut | 6 +- .../ucode/template/themes/shadcn/sysauth.ut | 48 +++- v2ray-geodata/Makefile | 4 +- 25 files changed, 614 insertions(+), 77 deletions(-) diff --git a/honk/Makefile b/honk/Makefile index 3640a34c..1891d295 100644 --- a/honk/Makefile +++ b/honk/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=honk -PKG_VERSION:=2026.11.03 +PKG_VERSION:=2026.11.04 PKG_RELEASE:=1 # Pinned honk-core revision (upstream daeuniverse/honk main), cross-compiled by @@ -14,10 +14,10 @@ PKG_RELEASE:=1 # assets behind CORE_COMMIT, so a plain feed build downloads and verifies them # without any generated file. A staged run may still override them through # honk/generated-stage.mk, which is generated and never committed. -CORE_COMMIT:=77176ff8e8cc2b75385a7030dc376536c382574a +CORE_COMMIT:=88d470c662e0ee7a3de3e67afc6129f67e66c1bf # Immutable source asset tag holding this pin's core archives. -HONK_SOURCE_TAG?=source-41594075839 +HONK_SOURCE_TAG?=source-41792584995 HONK_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HONK_SOURCE_TAG) -include $(CURDIR)/generated-stage.mk @@ -25,9 +25,9 @@ HONK_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HON HONK_ASSET_X86_64?=honk-core-$(CORE_COMMIT)-x86_64.tar.gz HONK_ASSET_AARCH64?=honk-core-$(CORE_COMMIT)-aarch64.tar.gz HONK_ASSET_ARMV7?=honk-core-$(CORE_COMMIT)-armv7.tar.gz -HONK_HASH_X86_64?=ad4801757f8b47505aa99cbdc47e4d606b8afe1a6269547d8073f68c20904a0e -HONK_HASH_AARCH64?=91a9a9929ec32d24de3c105f2a51d83a4975913a68e1446e57c2b1faf63adce6 -HONK_HASH_ARMV7?=a5b288c677698a44cc45084baf0bb67b1fb2b9ca624744a08d7a469093fc9870 +HONK_HASH_X86_64?=5a389e97296f6b5d3aad5a7180a57f9669845f6baa9b52742295de23147ac07e +HONK_HASH_AARCH64?=7261ed6b98b13fa3f7246175b5d9222feecdb3cdf3946b7c0bc296641866b345 +HONK_HASH_ARMV7?=1333e0598a493183af58e516173c66b44e692223949247cd32e53934dcebfeb4 # ARCH_PACKAGES is the package arch (e.g. arm_cortex-a7_neon-vfpv4); # ARCH is the CPU arch (x86_64/aarch64/arm) and coincides for 64-bit. diff --git a/honk/files/control.sh b/honk/files/control.sh index e166d089..8a8e97d9 100755 --- a/honk/files/control.sh +++ b/honk/files/control.sh @@ -376,6 +376,21 @@ cleanup_initialize() { fi } +# 交还面板管理时也用它,保证两条路径生成的 system.dae 一致。 +render_runtime_system() { + printf "global {\n wan_interface: auto\n lan_interface: '%s'\n" "$1" + printf " log_level: info\n dial_mode: domain\n allow_insecure: false\n" + printf " auto_config_kernel_parameter: true\n data_dir: '/etc/honk'\n" + printf " store_subscribe: true\n nfqueue_enable: true\n}\n\n" + printf "experimental {\n native_api {\n enabled: true\n listen: '%s:%s'\n" "$2" "$3" + printf " password_auth: true\n allow_anonymous_loopback: false\n" + printf " config_write: true\n ui: '/usr/share/doona'\n }\n" + if [ "$clash_api_enabled" = 1 ]; then + printf " clash_api {\n external_controller: '%s:%s'\n external_ui: '/usr/share/honk/zashboard'\n secret: '%s'\n default_mode: Rule\n }\n" "$2" "$clash_api_port" "$clash_api_secret" + fi + printf "}\n" +} + write_runtime_system() { lan_device=$1 lan_ip=$2 @@ -385,19 +400,7 @@ write_runtime_system() { current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}') [ "$current_hash" = "$system_written_hash" ] || { fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; } temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1 - { - printf "global {\n wan_interface: auto\n lan_interface: '%s'\n" "$lan_device" - printf " log_level: info\n dial_mode: domain\n allow_insecure: false\n" - printf " auto_config_kernel_parameter: true\n data_dir: '/etc/honk'\n" - printf " store_subscribe: true\n nfqueue_enable: true\n}\n\n" - printf "experimental {\n native_api {\n enabled: true\n listen: '%s:%s'\n" "$system_listen" "$system_port" - printf " password_auth: true\n allow_anonymous_loopback: false\n" - printf " config_write: true\n ui: '/usr/share/doona'\n }\n" - if [ "$clash_api_enabled" = 1 ]; then - printf " clash_api {\n external_controller: '%s:%s'\n external_ui: '/usr/share/honk/zashboard'\n secret: '%s'\n default_mode: Rule\n }\n" "$system_listen" "$clash_api_port" "$clash_api_secret" - fi - printf "}\n" - } > "$temp_system" || { rm -f "$temp_system"; return 1; } + render_runtime_system "$lan_device" "$system_listen" "$system_port" > "$temp_system" || { rm -f "$temp_system"; return 1; } chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; } current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}') [ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; } @@ -589,6 +592,232 @@ settings() { echo '{"ok":true,"message":"settings_saved"}' } +# 把 global/experimental 摊平成「路径=值」,取值和比对共用。 +system_entries() { + # 逐字符扫描:大括号可以跟值写在同一行,按整行判断会把归属解析错。 + awk ' + function flush(stmt, key, val, p, i, fields) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", stmt) + if (stmt == "" || d == 0) return + if (path[1] != "global" && path[1] != "experimental") return + if (stmt !~ /^[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:/) return + # 一行写多个字段无法可靠拆分,交给调用方拒绝,不能只取第一个就当成全部。 + fields = stmt + gsub(/"[^"]*"/, "", fields) + gsub(/\047[^\047]*\047/, "", fields) + if (gsub(/[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:/, ":", fields) > 1) { + print "!unsupported" + return + } + key = stmt + sub(/[[:space:]]*:.*/, "", key) + val = stmt + sub(/^[^:]*:[[:space:]]*/, "", val) + if (val ~ /^\047/) { val = substr(val, 2); sub(/\047.*/, "", val) } + else if (val ~ /^"/) { val = substr(val, 2); sub(/".*/, "", val) } + p = path[1] + for (i = 2; i <= d; i++) p = p "." path[i] + print p "." key "=" val + } + { + line = $0 + sub(/\r$/, "", line) + n = length(line) + for (i = 1; i <= n; i++) { + c = substr(line, i, 1) + if (quote != "") { + buf = buf c + if (c == quote) quote = "" + continue + } + if (c == "\047" || c == "\"") { quote = c; buf = buf c; continue } + if (c == "#") break + if (c == "{") { + name = buf + sub(/[[:space:]]*:.*/, "", name) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) + path[++d] = name + buf = "" + continue + } + if (c == "}") { + flush(buf) + buf = "" + if (d > 0) d-- + continue + } + buf = buf c + } + flush(buf) + buf = "" + } + ' "$1" +} + +entry_value() { + awk -v key="$1" 'index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }' "$2" +} + +# 只删顶级 global/experimental,在第一块的位置补 include,其余内容原样保留。 +strip_system_blocks() { + awk ' + skip { + n = length($0) + for (i = 1; i <= n; i++) { + c = substr($0, i, 1) + if (q != "") { if (c == q) q = ""; continue } + if (c == "\047" || c == "\"") { q = c; continue } + if (c == "#") break + if (c == "{") depth++ + else if (c == "}") depth-- + } + if (depth <= 0) skip = 0 + next + } + /^(global|experimental)[[:space:]]*\{/ { + if (!added) { print "include {\n system.dae\n}"; added = 1 } + depth = gsub(/\{/, "{") + depth -= gsub(/\}/, "}") + skip = depth > 0 + next + } + { print } + ' "$1" +} + +device_network() { + . /lib/functions/network.sh + network_get_device dn_dev "$lan_network" && [ "$dn_dev" = "$1" ] && { echo "$lan_network"; return 0; } + for dn_iface in $(ubus list 'network.interface.*' 2>/dev/null | sed 's/^network\.interface\.//'); do + network_get_device dn_dev "$dn_iface" || continue + [ "$dn_dev" = "$1" ] && { echo "$dn_iface"; return 0; } + done + return 1 +} + +valid_port() { + case "$1" in ''|*[!0-9]*) return 1 ;; esac + [ "$1" -ge 1024 ] && [ "$1" -le 65535 ] +} + +detach_fail() { + echo "{\"ok\":false,\"message\":\"$1\"}" + return 1 +} + +detach_rollback() { + dr_ok=1 + honk_lifecycle_stop 30 >/dev/null 2>&1 || dr_ok=0 + cp -p "$detach_dir/old-system.dae" /etc/honk/.system.dae.rollback && mv -f /etc/honk/.system.dae.rollback /etc/honk/system.dae || dr_ok=0 + cp -p "$detach_dir/old-config.dae" /etc/honk/.config.dae.rollback && mv -f /etc/honk/.config.dae.rollback /etc/honk/config.dae || dr_ok=0 + { uci import honk < "$detach_dir/old.uci" && uci commit honk && chmod 0600 /etc/config/honk; } || dr_ok=0 + if [ "$dr_ok" = 1 ] && [ "$old_running" = 1 ]; then honk_lifecycle_start 30 || dr_ok=0; fi + [ "$dr_ok" = 1 ] +} + +# 把文件里的 global/experimental 交还 system.dae 管理;dry_run 只返回会变动的项。 +detach() { + if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then detach_fail update_recovery_required; return 1; fi + . /usr/share/libubox/jshn.sh || { detach_fail jshn_unavailable; return 1; } + detach_request=$(head -c 4096) + dry_run= + [ -z "$detach_request" ] || json_load "$detach_request" 2>/dev/null || { detach_fail invalid_request; return 1; } + [ -z "$detach_request" ] || json_get_var dry_run dry_run + case "$dry_run" in 1|true) dry_run=1 ;; *) dry_run=0 ;; esac + load_settings + [ "$initialized" = 1 ] || { detach_fail not_initialized; return 1; } + [ "$settings_owner" = file ] || { detach_fail config_not_file_owned; return 1; } + [ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { detach_fail system_config_unavailable; return 1; } + umask 077 + detach_dir=$(mktemp -d /etc/honk/.detach.XXXXXX) || { detach_fail config_write_failed; return 1; } + trap 'rm -rf "$detach_dir"' 0 + old_hash=$(sha256sum /etc/honk/config.dae | awk '{print $1}') + system_entries /etc/honk/config.dae > "$detach_dir/old.entries" + grep -q '^!unsupported$' "$detach_dir/old.entries" && { detach_fail detach_layout_unsupported; return 1; } + + # 能对应到面板的值照搬,对不上的沿用面板现值。 + file_device=$(entry_value global.lan_interface "$detach_dir/old.entries") + if [ -n "$file_device" ] && detach_network=$(device_network "$file_device"); then lan_network=$detach_network; fi + file_port=$(entry_value experimental.native_api.listen "$detach_dir/old.entries") + file_port=${file_port##*:} + valid_port "$file_port" && listen_port=$file_port + old_clash_port=$clash_api_port + old_clash_secret=$clash_api_secret + clash_api_enabled=0 + if grep -q '^experimental\.clash_api\.' "$detach_dir/old.entries"; then + clash_api_enabled=1 + file_clash=$(entry_value experimental.clash_api.external_controller "$detach_dir/old.entries") + clash_api_port=${file_clash##*:} + clash_api_secret=$(entry_value experimental.clash_api.secret "$detach_dir/old.entries") + valid_clash_api || { clash_api_enabled=0; clash_api_port=$old_clash_port; clash_api_secret=$old_clash_secret; } + fi + check_network && network_settings || { detach_fail lan_network_unavailable; return 1; } + + render_runtime_system "$lan_device" "$lan_ip" "$listen_port" > "$detach_dir/system.dae" || { detach_fail config_write_failed; return 1; } + strip_system_blocks /etc/honk/config.dae > "$detach_dir/config.dae" || { detach_fail config_write_failed; return 1; } + [ "$(config_owner "$detach_dir/config.dae")" = system ] || { detach_fail detach_layout_unsupported; return 1; } + detach_check=$("$HONK_CORE" config validate --config "$detach_dir/config.dae" 2>/dev/null) + [ "$(printf '%s' "$detach_check" | jsonfilter -e '@.ok' 2>/dev/null)" = true ] || { detach_fail detach_validation_failed; return 1; } + + if [ "$dry_run" = 1 ]; then + system_entries "$detach_dir/system.dae" > "$detach_dir/new.entries" + json_init + json_add_boolean ok 1 + json_add_string message detach_preview + json_add_string lan_network "$lan_network" + json_add_int listen_port "$listen_port" + json_add_boolean clash_api_enabled "$clash_api_enabled" + json_add_int clash_api_port "$clash_api_port" + json_add_array changes + awk ' + { i = index($0, "="); k = substr($0, 1, i - 1); v = substr($0, i + 1) } + NR == FNR { seen[k] = 1; val[k] = v; next } + k ~ /\.secret$/ { if (!seen[k]) print k "\t***\t"; else if (val[k] != v) print k "\t***\t***"; next } + !seen[k] { print k "\t" v "\t"; next } + val[k] != v { print k "\t" v "\t" val[k] } + ' "$detach_dir/new.entries" "$detach_dir/old.entries" > "$detach_dir/changes" + while IFS=' ' read -r change_key change_old change_new; do + json_add_object "" + json_add_string key "$change_key" + json_add_string old "$change_old" + json_add_string new "$change_new" + json_close_object + done < "$detach_dir/changes" + json_close_array + json_dump + return 0 + fi + + honk_lock_acquire || { detach_fail operation_in_progress; return 1; } + trap 'honk_lock_release >/dev/null 2>&1; rm -rf "$detach_dir"' 0 + [ "$(sha256sum /etc/honk/config.dae | awk '{print $1}')" = "$old_hash" ] || { detach_fail config_changed_concurrently; return 1; } + old_running=0 + if managed_core_pid >/dev/null 2>&1; then + old_running=1 + elif pidof honk-core >/dev/null 2>&1; then + detach_fail unmanaged_core_running + return 1 + fi + cp -p /etc/honk/config.dae "$detach_dir/old-config.dae" && cp -p /etc/honk/system.dae "$detach_dir/old-system.dae" && uci export honk > "$detach_dir/old.uci" || { detach_fail config_write_failed; return 1; } + cp -p /etc/honk/config.dae /etc/honk/.config-before-detach.tmp && mv -f /etc/honk/.config-before-detach.tmp /etc/honk/config-before-detach.dae || { detach_fail config_write_failed; return 1; } + if [ "$old_running" = 1 ]; then honk_lifecycle_stop 30 || { detach_fail service_stop_unconfirmed; return 1; }; fi + chmod 0600 "$detach_dir/system.dae" "$detach_dir/config.dae" + if ! { cp -p "$detach_dir/system.dae" /etc/honk/.system.dae.detach && mv -f /etc/honk/.system.dae.detach /etc/honk/system.dae && \ + cp -p "$detach_dir/config.dae" /etc/honk/.config.dae.detach && mv -f /etc/honk/.config.dae.detach /etc/honk/config.dae && \ + uci set "honk.main.lan_network=$lan_network" && uci set "honk.main.listen_port=$listen_port" && \ + uci set "honk.main.clash_api_enabled=$clash_api_enabled" && uci set "honk.main.clash_api_port=$clash_api_port" && \ + uci set "honk.main.clash_api_secret=$clash_api_secret" && uci commit honk && chmod 0600 /etc/config/honk; } || \ + { [ "$old_running" = 1 ] && ! honk_lifecycle_start 30; }; then + if detach_rollback; then detach_fail detach_failed_rolled_back; else logger -t honk "detach rollback failed; snapshot kept at $detach_dir"; trap 'honk_lock_release >/dev/null 2>&1' 0; detach_fail detach_rollback_failed; fi + return 1 + fi + # 草稿原本就是旧配置时跟着换,免得配置页一直提示「有未应用的草稿」。 + if [ -f /etc/honk/config-draft.dae ] && [ "$(sha256sum /etc/honk/config-draft.dae | awk '{print $1}')" = "$old_hash" ]; then + cp -p /etc/honk/config.dae /etc/honk/.config-draft.detach && mv -f /etc/honk/.config-draft.detach /etc/honk/config-draft.dae + fi + echo '{"ok":true,"message":"config_detached"}' +} + diagnose() { load_settings errors="" @@ -972,8 +1201,11 @@ case "$1" in repair) repair ;; + detach) + detach + ;; *) - echo "usage: $0 {check|diagnose|preflight|local-direct|initialize|settings|repair}" >&2 + echo "usage: $0 {check|diagnose|preflight|local-direct|initialize|settings|repair|detach}" >&2 exit 2 ;; esac diff --git a/honk/files/lifecycle.sh b/honk/files/lifecycle.sh index d123a329..39e86e5e 100644 --- a/honk/files/lifecycle.sh +++ b/honk/files/lifecycle.sh @@ -1,6 +1,7 @@ #!/bin/sh HONK_LIFECYCLE_CORE=/usr/bin/honk-core +HONK_LIFECYCLE_HAVE_TIMEOUT=$(command -v timeout 2>/dev/null) # 字段归属:自带 global/experimental → file;只 include system.dae(且写法规范)→ system; # 其余一律 unknown。写法不规范时不猜,直接拒绝。 @@ -46,6 +47,15 @@ config_owner() { fi } +# 启动轮询用 0.25 秒粒度,核心就绪后尽快返回;没有 timeout 命令时退回 1 秒。 +honk_lifecycle_pause() { + if [ -n "$HONK_LIFECYCLE_HAVE_TIMEOUT" ]; then + timeout 0.25 sleep 1 2>/dev/null + return 0 + fi + sleep 1 +} + honk_lifecycle_procd_pid() { ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.pid' 2>/dev/null } @@ -227,6 +237,7 @@ honk_lifecycle_start() { # be told the start failed. Count distinct PIDs and give up early instead. hlp_seen_pid= hlp_attempts=0 + hlp_tick=0 while [ "$hlp_timeout" -gt 0 ]; do hlp_new_pid=$(honk_lifecycle_managed_pid 2>/dev/null) if [ -n "$hlp_new_pid" ] && [ "$hlp_new_pid" != "$hlp_seen_pid" ]; then @@ -264,8 +275,9 @@ honk_lifecycle_start() { hlp_pid= hlp_starttime= fi - sleep 1 - hlp_timeout=$((hlp_timeout - 1)) + honk_lifecycle_pause + hlp_tick=$((hlp_tick + 1)) + [ "$hlp_tick" -lt 4 ] || { hlp_tick=0; hlp_timeout=$((hlp_timeout - 1)); } done echo "honk lifecycle: new managed core did not become stable and own its API listener" >&2 return 1 diff --git a/luci-app-honk/Makefile b/luci-app-honk/Makefile index 0d00360a..b3f59675 100644 --- a/luci-app-honk/Makefile +++ b/luci-app-honk/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-honk -PKG_VERSION:=2026.11.03 +PKG_VERSION:=2026.11.04 PKG_RELEASE:=1 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) @@ -13,7 +13,7 @@ PKG_SOURCE:=doona-46e153cf-c6229ba102e8.tar.gz # Each package Makefile is evaluated on its own, so this tag is defined here too # instead of relying on the honk package's variable. It must match honk/Makefile: # scripts/check.sh enforces that. -HONK_SOURCE_TAG?=source-41594075839 +HONK_SOURCE_TAG?=source-41792584995 PKG_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HONK_SOURCE_TAG) PKG_SOURCE_SUBDIR:=doona PKG_HASH:=c6229ba102e867407540b3ec220595eff6ed63d7ae0037377432281977c9cf86 diff --git a/luci-app-honk/htdocs/luci-static/resources/view/honk/configuration.js b/luci-app-honk/htdocs/luci-static/resources/view/honk/configuration.js index 1811f15b..56e652b5 100644 --- a/luci-app-honk/htdocs/luci-static/resources/view/honk/configuration.js +++ b/luci-app-honk/htdocs/luci-static/resources/view/honk/configuration.js @@ -769,6 +769,10 @@ return view.extend({ parts.push(E('span', {}, _('An old manual draft from a previous version is kept on disk.'))); parts.push(draftButton(_('Load old draft'), 'legacy')); } + if (status && status.detach_backup) { + parts.push(E('span', {}, _('The configuration from before the switch is kept on disk.'))); + parts.push(draftButton(_('Load the previous configuration'), 'predetach')); + } draftNote.textContent = ''; if (!parts.length) { draftNote.hidden = true; diff --git a/luci-app-honk/htdocs/luci-static/resources/view/honk/overview.js b/luci-app-honk/htdocs/luci-static/resources/view/honk/overview.js index 2dcc5337..82328bb9 100644 --- a/luci-app-honk/htdocs/luci-static/resources/view/honk/overview.js +++ b/luci-app-honk/htdocs/luci-static/resources/view/honk/overview.js @@ -3,6 +3,7 @@ 'use strict'; 'require network'; 'require poll'; +'require ui'; 'require view'; 'require view.honk.rpc as honk'; 'require uci'; @@ -246,7 +247,15 @@ return view.extend({ form.appendChild(field(_('Clash API port'), clashPortInput)); form.appendChild(field(_('Clash API secret'), clashSecretInput, status.clash_api_secret_set ? _('A secret is set. Leave blank to keep it.') : _('Required when enabling. Use 6–128 letters, numbers, dots, underscores or hyphens.'))); form.appendChild(E('p', { 'class': 'honk-note' }, _('Use your existing HTTPS reverse proxy for external access. Enter the secret in Zashboard when connecting.'))); - form.appendChild(E('div', { 'class': 'cbi-page-actions' }, save)); + if (owner === 'file') { + const detach = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Switch to panel management')); + detach.addEventListener('click', function() { detachSettings(detach); }); + form.appendChild(E('div', { 'class': 'honk-actions' }, [ detach ])); + } else { + form.appendChild(E('div', { 'class': 'cbi-page-actions' }, [ save ])); + if (owner !== 'system') + form.appendChild(E('p', { 'class': 'honk-note' }, _('This configuration is not a plain include of system.dae, so these settings cannot be edited here.'))); + } form.appendChild(message); form.addEventListener('submit', function(ev) { ev.preventDefault(); @@ -292,6 +301,77 @@ return view.extend({ return card; } + /* 先预览会变动的项,确认后才真正改写。 */ + function detachSettings(button) { + button.disabled = true; + settingsBusy = true; + actionMessage.textContent = _('Checking the configuration…'); + honk.configDetach(true).then(honk.ensureOk).then(function(preview) { + actionMessage.textContent = ''; + return confirmDetach(preview); + }).then(function(confirmed) { + if (!confirmed) + return; + actionMessage.textContent = _('Switching to panel management…'); + return honk.configDetach(false).then(honk.ensureOk).then(function(result) { + actionMessage.textContent = honk.resultMessage(result, _('The panel manages these settings now.')); + }); + }).catch(function(error) { + actionMessage.textContent = honk.errorMessage(error, _('The switch failed. The configuration was left unchanged.')); + }).finally(function() { + settingsBusy = false; + button.disabled = false; + refresh(); + }); + } + + function confirmDetach(preview) { + /* 面板能映射的项与它固定写死的项分开列,避免把「会被默认值覆盖」说成「不支持」。 */ + const mapped = { + 'global.lan_interface': _('LAN network'), + 'experimental.native_api.listen': _('Doona listen address'), + 'experimental.clash_api.external_controller': _('Clash API address') + }; + const carried = []; + const replaced = []; + (preview.changes || []).forEach(function(item) { + if (/\.secret$/.test(item.key)) + return; + const label = mapped[item.key]; + if (label) + carried.push(E('li', {}, '%s:%s → %s'.format(label, item.old, item.new))); + else if (!item.new) + replaced.push(E('li', {}, _('%s: %s (removed)').format(item.key, item.old))); + else + replaced.push(E('li', {}, '%s:%s → %s'.format(item.key, item.old, item.new))); + }); + const summary = _('LAN network %s, port %s, Clash API %s.').format(preview.lan_network, preview.listen_port, + preview.clash_api_enabled ? _('enabled on port %s').format(preview.clash_api_port) : _('disabled')); + return new Promise(function(resolve) { + ui.showModal(_('Switch to panel management'), [ + E('p', {}, _('These settings move to this page. Subscriptions, nodes, groups and routing rules stay as they are.')), + E('p', {}, summary), + carried.length ? E('div', {}, [ E('p', {}, _('These values carry over, written the way the panel expects:')), E('ul', {}, carried) ]) : '', + replaced.length ? E('div', {}, [ E('p', {}, _('These items have no panel setting, so the panel defaults replace them:')), E('ul', {}, replaced) ]) : '', + E('p', { 'class': 'honk-note' }, _('Honk restarts during the switch.')), + E('p', { 'class': 'honk-note' }, _('The current file is backed up. To undo, load it again from the Configuration page and apply it.')), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': function(ev) { + ev.preventDefault(); + ui.hideModal(); + resolve(false); + } }, [ _('Cancel') ]), + ' ', + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function(ev) { + ev.preventDefault(); + ui.hideModal(); + resolve(true); + } }, [ _('Switch to panel management') ]) + ]) + ]); + }); + } + function renderSetup(status) { if (setupBusy) return; diff --git a/luci-app-honk/htdocs/luci-static/resources/view/honk/rpc.js b/luci-app-honk/htdocs/luci-static/resources/view/honk/rpc.js index 5aaed206..3601332f 100644 --- a/luci-app-honk/htdocs/luci-static/resources/view/honk/rpc.js +++ b/luci-app-honk/htdocs/luci-static/resources/view/honk/rpc.js @@ -68,6 +68,7 @@ const callLanCandidates = rpc.declare({ object: 'honk', method: 'lan_candidates' const callConfigValidate = rpc.declare({ object: 'honk', method: 'config_validate', params: [ 'content' ], expect: { '': {} } }); const callConfigSave = rpc.declare({ object: 'honk', method: 'config_save', params: [ 'content' ], expect: { '': {} } }); const callConfigApply = rpc.declare({ object: 'honk', method: 'config_apply', expect: { '': {} } }); +const callConfigDetach = rpc.declare({ object: 'honk', method: 'config_detach', params: [ 'dry_run' ], expect: { '': {} } }); const callRepair = rpc.declare({ object: 'honk', method: 'repair', expect: { '': {} } }); const callBackup = rpc.declare({ object: 'honk', method: 'backup', expect: { '': {} } }); const callRestore = rpc.declare({ object: 'honk', method: 'restore', expect: { '': {} } }); @@ -335,6 +336,12 @@ function tokenMessage(token) { config_validation_failed: _('The configuration did not pass static validation.'), config_validation_unavailable: _('The core validator did not return a result.'), config_settings_owned_by_file: _('这些设置由当前配置文件决定,请到「配置 → 编辑配置」里修改。'), + config_detached: _('已改由面板接管,订阅和分流规则保持不变。'), + config_not_file_owned: _('当前配置没有自带系统设置,不需要切换。'), + detach_layout_unsupported: _('配置里的 global / experimental 写法无法自动拆出,请到「配置 → 编辑配置」手动删除。'), + detach_validation_failed: _('拆分后的配置没有通过校验,原配置未改动。'), + detach_failed_rolled_back: _('切换后服务没能启动,已还原原配置。'), + detach_rollback_failed: _('切换失败且自动还原未完成,请到「维护」执行系统恢复。'), config_valid: _('配置通过校验。'), config_invalid: _('配置没有通过校验。'), config_saved_pending_apply: _('Configuration saved. The running service is unchanged until you apply it.'), @@ -465,6 +472,7 @@ return baseclass.extend({ configValidate: callConfigValidate, configSave: callConfigSave, configApply: callConfigApply, + configDetach: callConfigDetach, repair: callRepair, backup: callBackup, restore: callRestore, diff --git a/luci-app-honk/po/zh-cn/honk.po b/luci-app-honk/po/zh-cn/honk.po index ffd10771..67109a33 100644 --- a/luci-app-honk/po/zh-cn/honk.po +++ b/luci-app-honk/po/zh-cn/honk.po @@ -18,6 +18,9 @@ msgstr "%s(错误码:%s)" msgid "%s imported and Honk is running." msgstr "%s 已导入,Honk 正在运行。" +msgid "%s: %s (removed)" +msgstr "%s:%s(移除)" + msgid "/etc/honk/config.dae 不存在或不是普通文件。" msgstr "/etc/honk/config.dae 不存在或不是普通文件。" @@ -159,6 +162,9 @@ msgstr "选择 LAN 网卡" msgid "Clash API %s" msgstr "Clash API %s" +msgid "Clash API address" +msgstr "Clash API 地址" + msgid "Clash API port" msgstr "Clash API 端口" @@ -288,6 +294,9 @@ msgstr "已载入 /etc/honk/config.dae 的当前配置。" msgid "Custom" msgstr "自定义" +msgid "DNS" +msgstr "DNS" + msgid "Daily" msgstr "每天" @@ -318,6 +327,9 @@ msgstr "Doona 面板 %s" msgid "Doona is not ready yet. Check the overview for status." msgstr "Doona 尚未就绪,请在概况页查看状态。" +msgid "Doona listen address" +msgstr "Doona 监听地址" + msgid "Doona version" msgstr "Doona 版本" @@ -459,6 +471,9 @@ msgstr "Honk 报告了系统问题:%s" msgid "Honk restarted." msgstr "Honk 已重启。" +msgid "Honk restarts during the switch." +msgstr "切换过程中会重启 Honk。" + msgid "Honk settings" msgstr "Honk 设置" @@ -525,6 +540,11 @@ msgstr "局域网接口 %s" msgid "LAN network" msgstr "LAN 网络" +msgid "LAN network %s, port %s, Clash API %s." +msgstr "LAN 网络 %s,端口 %s,Clash API %s。" + + + msgid "Line %d" msgstr "第 %d 行" @@ -540,6 +560,9 @@ msgstr "载入草稿" msgid "Load old draft" msgstr "载入旧草稿" +msgid "Load the previous configuration" +msgstr "载入接管前的配置" + msgid "Loaded %s into the editor." msgstr "已把 %s 载入编辑器。" @@ -855,6 +878,9 @@ msgstr "订阅已保存,检测到可用节点。" msgid "Subscriptions and nodes must use a protocol Honk supports; SSR is not supported." msgstr "订阅与节点需使用 Honk 支持的协议,不支持 SSR。" +msgid "Switching to panel management…" +msgstr "正在改由面板接管…" + msgid "System check and repair" msgstr "系统检查与修复" @@ -897,6 +923,9 @@ msgstr "The configuration file exceeds the 2 MiB limit." msgid "The configuration file is missing." msgstr "配置文件不存在。" +msgid "The configuration from before the switch is kept on disk." +msgstr "面板接管前的配置已备份在路由器上。" + msgid "The configuration request is empty or invalid." msgstr "The configuration request is empty or invalid." @@ -909,6 +938,9 @@ msgstr "The core validator did not return a result." msgid "The curl command is missing. Install the curl package and try again." msgstr "缺少 curl 命令,请先安装 curl 包(apk add curl 或 opkg install curl)后重试。" +msgid "The current file is backed up. To undo, load it again from the Configuration page and apply it." +msgstr "原配置已备份。想撤回:到「配置」页点「载入接管前的配置」,再保存并应用。" + msgid "The dae file contains invalid sections or unsupported includes." msgstr "dae 文件包含无效配置段或不支持的 include。" @@ -957,6 +989,9 @@ msgstr "Honk 托管服务状态异常。" msgid "The operation failed." msgstr "操作失败。" +msgid "The panel manages these settings now." +msgstr "已改由面板接管,订阅和分流规则保持不变。" + msgid "The password must be between 8 and 128 characters." msgstr "密码长度必须在 8 到 128 个字符之间。" @@ -1005,6 +1040,9 @@ msgstr "订阅无效或名称已存在。" msgid "The subscription returned no usable nodes. The configuration was not changed." msgstr "订阅没有返回可用节点,配置未改动。" +msgid "The switch failed. The configuration was left unchanged." +msgstr "切换失败,原配置未改动。" + msgid "The system check did not finish (interface timeout or error). Retry, or check the core log under Maintenance → Logs." msgstr "系统检查没有跑完(接口超时或出错),请重试;或到「维护 → 日志」查看核心日志。" @@ -1029,9 +1067,21 @@ msgstr "上传的 dae 文件无效。" msgid "There is no saved draft to apply." msgstr "没有可应用的已保存草稿。" +msgid "These items have no panel setting, so the panel defaults replace them:" +msgstr "这些项面板没有对应设置,会改成面板默认值:" + msgid "These lines will be changed for this router:" msgstr "这几行会按本机修改:" +msgid "These settings move to this page. Subscriptions, nodes, groups and routing rules stay as they are." +msgstr "这些设置改由概览页管理,订阅、节点、分组和分流规则不变。" + +msgid "These values carry over, written the way the panel expects:" +msgstr "这些值会保留,只是写法按面板要求调整:" + +msgid "This configuration is not a plain include of system.dae, so these settings cannot be edited here." +msgstr "当前配置不是纯粹的 system.dae 引用,这里无法编辑这些设置。" + msgid "This core cannot use any of these nodes, so the subscription was not saved." msgstr "这份订阅里的节点本内核一个都用不了,所以没有保存。" @@ -1104,6 +1154,12 @@ msgstr "YAML 解析器加载失败" msgid "Zashboard is still downloading or unavailable." msgstr "Zashboard 仍在下载或暂不可用。" +msgid "disabled" +msgstr "关闭" + +msgid "enabled on port %s" +msgstr "开启(端口 %s)" + msgid "global settings" msgstr "全局设置" @@ -1209,6 +1265,12 @@ msgstr "写入 system.dae 失败,已回滚。" msgid "写入 system.dae 的监听地址失败,请检查文件权限。" msgstr "写入 system.dae 的监听地址失败,请检查文件权限。" +msgid "切换后服务没能启动,已还原原配置。" +msgstr "切换后服务没能启动,已还原原配置。" + +msgid "切换失败且自动还原未完成,请到「维护」执行系统恢复。" +msgstr "切换失败且自动还原未完成,请到「维护」执行系统恢复。" + msgid "创建 Doona 管理员失败,请到「维护 → 日志」查看核心日志。" msgstr "创建 Doona 管理员失败,请到「维护 → 日志」查看核心日志。" @@ -1233,12 +1295,27 @@ msgstr "备份现有 system.dae 失败,请检查磁盘空间。" msgid "备份系统配置失败,请检查磁盘空间。" msgstr "备份系统配置失败,请检查磁盘空间。" +msgid "已改由面板接管,订阅和分流规则保持不变。" +msgstr "已改由面板接管,订阅和分流规则保持不变。" + +msgid "已经初始化过了,请用原来的账号登录 Doona。忘记密码:到「维护 → 重置管理员」清掉管理员记录(不动订阅和规则),再回到本页重新初始化。" +msgstr "已经初始化过了,请用原来的账号登录 Doona。忘记密码:到「维护 → 重置管理员」清掉管理员记录(不动订阅和规则),再回到本页重新初始化。" + msgid "应用设置失败且回滚未完成,快照已保留,请执行系统恢复。" msgstr "应用设置失败且回滚未完成,快照已保留,请执行系统恢复。" msgid "开关类设置取值不合法。" msgstr "开关类设置取值不合法。" +msgid "当前配置不支持快捷追加,请在「编辑配置」中修改。" +msgstr "当前配置不支持快捷追加,请在「编辑配置」中修改。" + +msgid "当前配置没有 include { system.dae },这些设置请到「编辑配置」里修改。" +msgstr "当前配置没有 include { system.dae },这些设置请到「编辑配置」里修改。" + +msgid "当前配置没有自带系统设置,不需要切换。" +msgstr "当前配置没有自带系统设置,不需要切换。" + msgid "找不到 /usr/bin/honk-core,请重新安装 honk 包。" msgstr "找不到 /usr/bin/honk-core,请重新安装 honk 包。" @@ -1248,6 +1325,9 @@ msgstr "找不到该任务。" msgid "抓不到这个订阅地址:网络不通、需要代理,或链接已失效。" msgstr "抓不到这个订阅地址:网络不通、需要代理,或链接已失效。" +msgid "拆分后的配置没有通过校验,原配置未改动。" +msgstr "拆分后的配置没有通过校验,原配置未改动。" + msgid "拿不到任务编号。" msgstr "拿不到任务编号。" @@ -1344,35 +1424,29 @@ msgstr "读不到任务状态。" msgid "读不到核心的 API 状态,请查看日志。" msgstr "读不到核心的 API 状态,请查看日志。" +msgid "这些设置由当前配置文件决定,请到「配置 → 编辑配置」里修改。" +msgstr "这些设置由当前配置文件决定,请到「配置 → 编辑配置」里修改。" + msgid "选中的 LAN 网络没有 IPv4 地址。" msgstr "选中的 LAN 网络没有 IPv4 地址。" +msgid "配置没有通过校验。" +msgstr "配置没有通过校验。" + msgid "配置没有通过静态校验。" msgstr "配置没有通过静态校验。" msgid "配置路径不是 /etc/honk/config.dae,拒绝操作。" msgstr "配置路径不是 /etc/honk/config.dae,拒绝操作。" -msgid "预检失败,请稍后重试。" -msgstr "预检失败,请稍后重试。" - -msgid "DNS" -msgstr "DNS" - -msgid "已经初始化过了,请用原来的账号登录 Doona。忘记密码:到「维护 → 重置管理员」清掉管理员记录(不动订阅和规则),再回到本页重新初始化。" -msgstr "已经初始化过了,请用原来的账号登录 Doona。忘记密码:到「维护 → 重置管理员」清掉管理员记录(不动订阅和规则),再回到本页重新初始化。" - -msgid "当前配置没有 include { system.dae },这些设置请到「编辑配置」里修改。" -msgstr "当前配置没有 include { system.dae },这些设置请到「编辑配置」里修改。" - -msgid "这些设置由当前配置文件决定,请到「配置 → 编辑配置」里修改。" -msgstr "这些设置由当前配置文件决定,请到「配置 → 编辑配置」里修改。" - -msgid "配置没有通过校验。" -msgstr "配置没有通过校验。" - msgid "配置通过校验。" msgstr "配置通过校验。" -msgid "当前配置不支持快捷追加,请在「编辑配置」中修改。" -msgstr "当前配置不支持快捷追加,请在「编辑配置」中修改。" +msgid "配置里的 global / experimental 写法无法自动拆出,请到「配置 → 编辑配置」手动删除。" +msgstr "配置里的 global / experimental 写法无法自动拆出,请到「配置 → 编辑配置」手动删除。" + +msgid "预检失败,请稍后重试。" +msgstr "预检失败,请稍后重试。" + +msgid "Switch to panel management" +msgstr "切换为面板管理" diff --git a/luci-app-honk/root/usr/libexec/rpcd/honk b/luci-app-honk/root/usr/libexec/rpcd/honk index d9d3fa80..ff183ab2 100755 --- a/luci-app-honk/root/usr/libexec/rpcd/honk +++ b/luci-app-honk/root/usr/libexec/rpcd/honk @@ -51,7 +51,7 @@ lock_owner_invalid() { } status() { - local enabled initialized boot_enabled running api_url api_ready core_version core_commit doona_version kernel lan_network listen_port errors listen clash_api_enabled clash_api_port clash_api_secret clash_api_url clash_api_ready clash_ui_ready clash_secret_set settings_owner api_enabled core_ready config_pending draft_pending legacy_draft clash_listen_addr + local enabled initialized boot_enabled running api_url api_ready core_version core_commit doona_version kernel lan_network listen_port errors listen clash_api_enabled clash_api_port clash_api_secret clash_api_url clash_api_ready clash_ui_ready clash_secret_set settings_owner api_enabled core_ready config_pending draft_pending legacy_draft detach_backup clash_listen_addr config_load honk config_get_bool enabled main enabled 0 config_get_bool initialized main initialized 0 @@ -74,6 +74,7 @@ status() { config_pending=0 draft_pending=0 legacy_draft=0 + detach_backup=0 listen= runtime_config=$("$CORE" config validate --config /etc/honk/config.dae 2>/dev/null) if [ -n "$runtime_config" ]; then @@ -120,6 +121,7 @@ status() { [ "$draft_hash" = "$active_hash" ] || draft_pending=1 fi [ -f /etc/honk/manual-config.dae ] && [ ! -L /etc/honk/manual-config.dae ] && legacy_draft=1 + [ -f /etc/honk/config-before-detach.dae ] && [ ! -L /etc/honk/config-before-detach.dae ] && detach_backup=1 clash_api_ready=0 if [ "$running" = 1 ] && [ "$clash_api_enabled" = 1 ] && [ "$clash_api_port" -gt 0 ] && honk_lifecycle_listener_owned "$managed_pid" "$clash_api_port"; then clash_api_ready=1; fi clash_ui_ready=0 @@ -155,6 +157,7 @@ status() { json_add_boolean config_pending "$config_pending" json_add_boolean draft_pending "$draft_pending" json_add_boolean legacy_draft "$legacy_draft" + json_add_boolean detach_backup "$detach_backup" json_add_boolean core_ready "$core_ready" json_add_boolean api_enabled "$api_enabled" json_add_boolean api_ready "$api_ready" @@ -912,6 +915,7 @@ config_draft_read() { case "$draft_source" in draft) draft_file=/etc/honk/config-draft.dae ;; legacy) draft_file=/etc/honk/manual-config.dae ;; + predetach) draft_file=/etc/honk/config-before-detach.dae ;; *) emit_result false invalid_request; return 1 ;; esac [ -f "$draft_file" ] && [ ! -L "$draft_file" ] || { emit_result false config_draft_missing; return 1; } @@ -1132,7 +1136,7 @@ config_apply() { case "$1" in list) - echo '{"status":{},"check":{},"initialize":{"username":"String","password":"String"},"start":{},"stop":{},"restart":{},"settings":{"lan_network":"String","listen_port":"Integer","boot_enabled":"Boolean","clash_api_enabled":"Boolean","clash_api_port":"Integer","clash_api_secret":"String"},"add_subscription":{"name":"String","url":"String"},"probe_subscription":{"url":"String"},"reset_admin":{},"add_nodes":{"links":"String"},"import_dae":{},"config_read":{},"config_draft_read":{},"lan_candidates":{},"config_validate":{"content":"String"},"config_save":{"content":"String"},"config_apply":{},"repair":{},"backup":{},"restore":{},"restore_prepare":{},"reset":{},"job_status":{"job_id":"String"},"logs":{"source":"String","level":"String","limit":"Integer"},"update_check":{},"update_apply":{}}' + echo '{"status":{},"check":{},"initialize":{"username":"String","password":"String"},"start":{},"stop":{},"restart":{},"settings":{"lan_network":"String","listen_port":"Integer","boot_enabled":"Boolean","clash_api_enabled":"Boolean","clash_api_port":"Integer","clash_api_secret":"String"},"add_subscription":{"name":"String","url":"String"},"probe_subscription":{"url":"String"},"reset_admin":{},"add_nodes":{"links":"String"},"import_dae":{},"config_read":{},"config_draft_read":{},"lan_candidates":{},"config_validate":{"content":"String"},"config_save":{"content":"String"},"config_apply":{},"config_detach":{"dry_run":"Boolean"},"repair":{},"backup":{},"restore":{},"restore_prepare":{},"reset":{},"job_status":{"job_id":"String"},"logs":{"source":"String","level":"String","limit":"Integer"},"update_check":{},"update_apply":{}}' ;; call) case "$2" in @@ -1181,6 +1185,9 @@ list) config_apply) config_apply ;; + config_detach) + exec "$CONTROL" detach + ;; repair) exec "$CONTROL" repair ;; diff --git a/luci-app-honk/root/usr/share/rpcd/acl.d/luci-app-honk.json b/luci-app-honk/root/usr/share/rpcd/acl.d/luci-app-honk.json index cd17410a..693dfa3f 100644 --- a/luci-app-honk/root/usr/share/rpcd/acl.d/luci-app-honk.json +++ b/luci-app-honk/root/usr/share/rpcd/acl.d/luci-app-honk.json @@ -81,7 +81,8 @@ "update_apply", "config_read", "config_draft_read", - "config_apply" + "config_apply", + "config_detach" ] }, "file": { diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile index 890556ff..433c570d 100644 --- a/luci-app-ssr-plus/Makefile +++ b/luci-app-ssr-plus/Makefile @@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus LUCI_PKGARCH:=all PKG_NAME:=luci-app-ssr-plus PKG_VERSION:=198 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ diff --git a/luci-theme-aurora/.dev/src/media/login.css b/luci-theme-aurora/.dev/src/media/login.css index df914819..9765c9c0 100644 --- a/luci-theme-aurora/.dev/src/media/login.css +++ b/luci-theme-aurora/.dev/src/media/login.css @@ -159,6 +159,10 @@ body[data-bg] .login-card { .login-error { @apply bg-danger-surface text-danger border-hairline rounded-2xl border px-3 py-2 text-center text-sm; + + &.warning { + @apply bg-warning-surface text-warning; + } } .login-actions { diff --git a/luci-theme-aurora/.dev/tests/build-performance.test.js b/luci-theme-aurora/.dev/tests/build-performance.test.js index b254a50e..210ea62e 100644 --- a/luci-theme-aurora/.dev/tests/build-performance.test.js +++ b/luci-theme-aurora/.dev/tests/build-performance.test.js @@ -19,7 +19,8 @@ test("production assets stay within raw-transfer budgets", () => { // 187K: pseudo-elements are nested rules, one content declaration each. assert.ok(main <= 187_000, "main.css exceeds 187 KB"); - assert.ok(login <= 12_000, "login.css exceeds 12 KB"); + // 12.5K: auth_plugin warning tokens added ~0.35 KB. + assert.ok(login <= 12_500, "login.css exceeds 12.5 KB"); // 22K: palette recents (record on pick, pure-LRU browse order, storage // validation) added ~0.8 KB and the ">" logout command ~0.2 KB. The total // budget below moved by the same amount. diff --git a/luci-theme-aurora/Makefile b/luci-theme-aurora/Makefile index 32fed594..ad2888e1 100644 --- a/luci-theme-aurora/Makefile +++ b/luci-theme-aurora/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS) LUCI_DEPENDS:=+luci-base -PKG_VERSION:=1.4.2 +PKG_VERSION:=1.4.3 PKG_RELEASE:=20261104 PKG_LICENSE:=Apache-2.0 diff --git a/luci-theme-aurora/htdocs/luci-static/aurora/login.css b/luci-theme-aurora/htdocs/luci-static/aurora/login.css index b8a2a3b3..e5ade23c 100644 --- a/luci-theme-aurora/htdocs/luci-static/aurora/login.css +++ b/luci-theme-aurora/htdocs/luci-static/aurora/login.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content:"";--tw-shadow:0 0 #0000;--tw-inset-shadow:0 0 #0000;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base) * 2);--animate-spin:spin 1s linear infinite}}@layer base{*,:before,:after{box-sizing:border-box}body,hr,p,button,input{margin:0}button,input{font:inherit}a{text-decoration-line:none}[hidden]{display:none!important}}@layer components,utilities;:root{--bg:#f7fafc;--surface:#fff;--text:#121a22;--brand:#0085b7;--on-brand:#fff;--danger:#8d1925;--text-muted:#5f666d;--text-subtle:#7f858b;--hairline:#121a2221;--brand-subtle:#ddecf5;--danger-surface:#ffe8e6;--control-bg:#fff;--app-shadow-md:0 4px 16px #00000014, 0 1px 3px #0000000a;--font-sans:"Lato", ui-sans-serif, system-ui, sans-serif;--spacing:.25rem;--radius-base:.5rem}@supports (color:lab(0% 0 0)){:root{--bg:lab(98.1553% -.760227 -1.25223);--surface:lab(100% 0 0);--text:lab(8.89727% -1.68239 -6.93074);--brand:lab(51.3503% -19.237 -40.2116);--on-brand:lab(100% 0 0);--danger:lab(31.1495% 47.9256 25.8661);--text-muted:lab(42.8254% -1.57365 -4.8763);--text-subtle:lab(55.3248% -1.4126 -4.053);--hairline:lab(8.89727% -1.68239 -6.93074/.13);--brand-subtle:lab(92.5794% -3.9458 -6.17858);--danger-surface:lab(93.849% 11.2841 5.29771);--control-bg:lab(100.011% -.760317 -1.25225);--app-shadow-md:0 4px 16px lab(0% 0 0/.08), 0 1px 3px lab(0% 0 0/.04)}}[data-darkmode=true]{--bg:#05070e;--surface:#141822;--text:#f9fafb;--brand:#00958e;--on-brand:#fff;--danger:#f17070;--text-muted:#909297;--text-subtle:#5d6067;--hairline:#f9fafb1a;--brand-subtle:#061a20;--danger-surface:#541f20;--control-bg:#0a0e17;--app-shadow-md:0 10px 28px #0000006b}@supports (color:lab(0% 0 0)){[data-darkmode=true]{--bg:lab(1.93894% .0855997 -3.22397);--surface:lab(8.24713% -.0405461 -7.31381);--text:lab(98.252% -.0618994 -.756907);--brand:lab(55.0978% -44.5567 -7.62978);--on-brand:lab(100% 0 0);--danger:lab(63.5705% 50.8105 25.3639);--text-muted:lab(60.5379% -.225931 -3.05105);--text-subtle:lab(40.6865% -.278354 -4.23647);--hairline:lab(98.252% -.0618994 -.756907/.1);--brand-subtle:lab(7.95734% -6.34954 -6.72844);--danger-surface:lab(20.3014% 25.4342 12.827);--control-bg:lab(3.9802% .0812709 -5.64623);--app-shadow-md:0 10px 28px lab(0% 0 0/.42)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-property:none!important;animation:none!important}.spinning:before{animation:var(--animate-spin)!important}}html{background-color:var(--bg);min-height:100dvh;font-family:var(--font-sans);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}html[data-darkmode=true]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}html body{background-color:var(--bg);min-height:100dvh}.page-bg{pointer-events:none;background-image:var(--page-bg-lqip,var(--page-bg));background-position:50%;background-repeat:no-repeat;background-size:cover;position:fixed;inset:0;transform:translateZ(0)}.page-bg:after{pointer-events:none;background-image:var(--page-bg);opacity:0;--tw-content:"";content:var(--tw-content);background-position:50%;background-repeat:no-repeat;background-size:cover;transition:opacity .5s;position:absolute;inset:0}.page-bg.full-loaded:after{opacity:1}@keyframes divider-in{0%{opacity:0;transform:scaleX(.15)}}.login-screen{min-height:100dvh;padding-inline:calc(var(--spacing) * 5);padding-bottom:calc(var(--spacing) * 14);flex-direction:column;justify-content:center;align-items:center;display:flex;position:relative}.login-shell{width:100%;max-width:var(--container-md)}.page-bg{--page-bg:var(--login-bg);--page-bg-lqip:var(--login-bg-lqip)}body[data-bg] .page-bg:before{pointer-events:none;z-index:1;--tw-content:"";content:var(--tw-content);background:var(--bg);position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){body[data-bg] .page-bg:before{background:color-mix(in srgb,var(--bg) var(--login-bg-scrim,0%),transparent)}}body[data-bg] .login-card{-webkit-backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);background:var(--surface)}@supports (color:color-mix(in lab, red, red)){body[data-bg] .login-card{background:color-mix(in srgb,var(--surface) var(--login-bg-alpha,100%),transparent)}}@media (prefers-reduced-transparency:reduce){body[data-bg] .login-card{--login-bg-alpha:100%;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}}.login-card{gap:calc(var(--spacing) * 7);border-radius:calc(var(--radius-base) * 4);background-color:var(--surface);padding:calc(var(--spacing) * 14);--tw-shadow:var(--app-shadow-md);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;display:flex}@media not all and (width>=48rem){.login-card{border-radius:calc(var(--radius-base) * 3);padding:calc(var(--spacing) * 9)}}.login-brand{align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.login-divider{border-style:var(--tw-border-style);--tw-gradient-position:to right;border-width:0;width:100%;height:1px;animation:.7s cubic-bezier(.16,1,.3,1) .1s both divider-in}@supports (background-image:linear-gradient(in lab, red, red)){.login-divider{--tw-gradient-position:to right in oklab}}.login-divider{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:transparent;--tw-gradient-via:var(--hairline);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));--tw-gradient-to:transparent}.login-logo{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:calc(var(--radius-base) * 2);background-color:var(--brand-subtle);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--brand-subtle);flex-shrink:0;justify-content:center;align-items:center;display:flex}.login-logo img{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6)}.login-title{gap:var(--spacing);flex-direction:column;display:flex}.login-hostname{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);color:var(--text)}.login-subtitle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text)}.login-form{gap:calc(var(--spacing) * 5);flex-direction:column;display:flex}.login-fields{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.login-field{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.login-label{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-widest);color:var(--text);text-transform:uppercase}.input-wrap{border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);background-color:var(--control-bg);contain:layout style;position:relative}.input-wrap:after{pointer-events:none;border-radius:calc(var(--radius-2xl) + 1px);border-style:var(--tw-border-style);border-width:2px;border-color:var(--brand);opacity:0;--tw-content:"";content:var(--tw-content);transition:opacity .15s;position:absolute;inset:-1px}.input-wrap:focus-within:after{opacity:1}.login-input{appearance:none;border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--text);background-color:#0000;border-width:0;outline-style:none}.login-input::placeholder{color:var(--text-subtle)}.login-error{border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);background-color:var(--danger-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--danger)}.login-actions{padding-top:calc(var(--spacing) * 4)}.login-submit{cursor:pointer;border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);background-color:var(--brand);width:100%;padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));font-weight:var(--font-weight-medium);color:var(--on-brand);border-width:0}@media (hover:hover){.login-submit:hover{opacity:.9}}.login-submit:active{opacity:.8}.login-footer{right:0;bottom:calc(var(--spacing) * 5);padding-inline:calc(var(--spacing) * 5);text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--text-muted);position:absolute;left:0}.login-footer p{white-space:normal}.login-footer a{color:var(--brand)}@media (hover:hover){.login-footer a:hover{text-decoration-line:underline}}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@keyframes spin{to{transform:rotate(360deg)}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content:"";--tw-shadow:0 0 #0000;--tw-inset-shadow:0 0 #0000;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base) * 2);--animate-spin:spin 1s linear infinite}}@layer base{*,:before,:after{box-sizing:border-box}body,hr,p,button,input{margin:0}button,input{font:inherit}a{text-decoration-line:none}[hidden]{display:none!important}}@layer components,utilities;:root{--bg:#f7fafc;--surface:#fff;--text:#121a22;--brand:#0085b7;--on-brand:#fff;--warning:#643e00;--danger:#8d1925;--text-muted:#5f666d;--text-subtle:#7f858b;--hairline:#121a2221;--brand-subtle:#ddecf5;--warning-surface:#ffedd6;--danger-surface:#ffe8e6;--control-bg:#fff;--app-shadow-md:0 4px 16px #00000014, 0 1px 3px #0000000a;--font-sans:"Lato", ui-sans-serif, system-ui, sans-serif;--spacing:.25rem;--radius-base:.5rem}@supports (color:lab(0% 0 0)){:root{--bg:lab(98.1553% -.760227 -1.25223);--surface:lab(100% 0 0);--text:lab(8.89727% -1.68239 -6.93074);--brand:lab(51.3503% -19.237 -40.2116);--on-brand:lab(100% 0 0);--warning:lab(30.2236% 14.0359 42.8654);--danger:lab(31.1495% 47.9256 25.8661);--text-muted:lab(42.8254% -1.57365 -4.8763);--text-subtle:lab(55.3248% -1.4126 -4.053);--hairline:lab(8.89727% -1.68239 -6.93074/.13);--brand-subtle:lab(92.5794% -3.9458 -6.17858);--warning-surface:lab(94.8007% 4.75499 15.6111);--danger-surface:lab(93.849% 11.2841 5.29771);--control-bg:lab(100.011% -.760317 -1.25225);--app-shadow-md:0 4px 16px lab(0% 0 0/.08), 0 1px 3px lab(0% 0 0/.04)}}[data-darkmode=true]{--bg:#05070e;--surface:#141822;--text:#f9fafb;--brand:#00958e;--on-brand:#fff;--warning:#f0ba59;--danger:#f17070;--text-muted:#909297;--text-subtle:#5d6067;--hairline:#f9fafb1a;--brand-subtle:#061a20;--warning-surface:#45310b;--danger-surface:#541f20;--control-bg:#0a0e17;--app-shadow-md:0 10px 28px #0000006b}@supports (color:lab(0% 0 0)){[data-darkmode=true]{--bg:lab(1.93894% .0855997 -3.22397);--surface:lab(8.24713% -.0405461 -7.31381);--text:lab(98.252% -.0618994 -.756907);--brand:lab(55.0978% -44.5567 -7.62978);--on-brand:lab(100% 0 0);--warning:lab(79.1996% 12.6808 55.9505);--danger:lab(63.5705% 50.8105 25.3639);--text-muted:lab(60.5379% -.225931 -3.05105);--text-subtle:lab(40.6865% -.278354 -4.23647);--hairline:lab(98.252% -.0618994 -.756907/.1);--brand-subtle:lab(7.95734% -6.34954 -6.72844);--warning-surface:lab(22.2973% 6.18184 26.7927);--danger-surface:lab(20.3014% 25.4342 12.827);--control-bg:lab(3.9802% .0812709 -5.64623);--app-shadow-md:0 10px 28px lab(0% 0 0/.42)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-property:none!important;animation:none!important}.spinning:before{animation:var(--animate-spin)!important}}html{background-color:var(--bg);min-height:100dvh;font-family:var(--font-sans);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}html[data-darkmode=true]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}html body{background-color:var(--bg);min-height:100dvh}.page-bg{pointer-events:none;background-image:var(--page-bg-lqip,var(--page-bg));background-position:50%;background-repeat:no-repeat;background-size:cover;position:fixed;inset:0;transform:translateZ(0)}.page-bg:after{pointer-events:none;background-image:var(--page-bg);opacity:0;--tw-content:"";content:var(--tw-content);background-position:50%;background-repeat:no-repeat;background-size:cover;transition:opacity .5s;position:absolute;inset:0}.page-bg.full-loaded:after{opacity:1}@keyframes divider-in{0%{opacity:0;transform:scaleX(.15)}}.login-screen{min-height:100dvh;padding-inline:calc(var(--spacing) * 5);padding-bottom:calc(var(--spacing) * 14);flex-direction:column;justify-content:center;align-items:center;display:flex;position:relative}.login-shell{width:100%;max-width:var(--container-md)}.page-bg{--page-bg:var(--login-bg);--page-bg-lqip:var(--login-bg-lqip)}body[data-bg] .page-bg:before{pointer-events:none;z-index:1;--tw-content:"";content:var(--tw-content);background:var(--bg);position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){body[data-bg] .page-bg:before{background:color-mix(in srgb,var(--bg) var(--login-bg-scrim,0%),transparent)}}body[data-bg] .login-card{-webkit-backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);background:var(--surface)}@supports (color:color-mix(in lab, red, red)){body[data-bg] .login-card{background:color-mix(in srgb,var(--surface) var(--login-bg-alpha,100%),transparent)}}@media (prefers-reduced-transparency:reduce){body[data-bg] .login-card{--login-bg-alpha:100%;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}}.login-card{gap:calc(var(--spacing) * 7);border-radius:calc(var(--radius-base) * 4);background-color:var(--surface);padding:calc(var(--spacing) * 14);--tw-shadow:var(--app-shadow-md);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;display:flex}@media not all and (width>=48rem){.login-card{border-radius:calc(var(--radius-base) * 3);padding:calc(var(--spacing) * 9)}}.login-brand{align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.login-divider{border-style:var(--tw-border-style);--tw-gradient-position:to right;border-width:0;width:100%;height:1px;animation:.7s cubic-bezier(.16,1,.3,1) .1s both divider-in}@supports (background-image:linear-gradient(in lab, red, red)){.login-divider{--tw-gradient-position:to right in oklab}}.login-divider{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:transparent;--tw-gradient-via:var(--hairline);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));--tw-gradient-to:transparent}.login-logo{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:calc(var(--radius-base) * 2);background-color:var(--brand-subtle);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--brand-subtle);flex-shrink:0;justify-content:center;align-items:center;display:flex}.login-logo img{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6)}.login-title{gap:var(--spacing);flex-direction:column;display:flex}.login-hostname{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);color:var(--text)}.login-subtitle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text)}.login-form{gap:calc(var(--spacing) * 5);flex-direction:column;display:flex}.login-fields{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.login-field{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.login-label{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-widest);color:var(--text);text-transform:uppercase}.input-wrap{border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);background-color:var(--control-bg);contain:layout style;position:relative}.input-wrap:after{pointer-events:none;border-radius:calc(var(--radius-2xl) + 1px);border-style:var(--tw-border-style);border-width:2px;border-color:var(--brand);opacity:0;--tw-content:"";content:var(--tw-content);transition:opacity .15s;position:absolute;inset:-1px}.input-wrap:focus-within:after{opacity:1}.login-input{appearance:none;border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--text);background-color:#0000;border-width:0;outline-style:none}.login-input::placeholder{color:var(--text-subtle)}.login-error{border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);background-color:var(--danger-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--danger)}.login-error.warning{background-color:var(--warning-surface);color:var(--warning)}.login-actions{padding-top:calc(var(--spacing) * 4)}.login-submit{cursor:pointer;border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);background-color:var(--brand);width:100%;padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));font-weight:var(--font-weight-medium);color:var(--on-brand);border-width:0}@media (hover:hover){.login-submit:hover{opacity:.9}}.login-submit:active{opacity:.8}.login-footer{right:0;bottom:calc(var(--spacing) * 5);padding-inline:calc(var(--spacing) * 5);text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--text-muted);position:absolute;left:0}.login-footer p{white-space:normal}.login-footer a{color:var(--brand)}@media (hover:hover){.login-footer a:hover{text-decoration-line:underline}}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@keyframes spin{to{transform:rotate(360deg)}} \ No newline at end of file diff --git a/luci-theme-aurora/ucode/template/themes/aurora/sysauth.ut b/luci-theme-aurora/ucode/template/themes/aurora/sysauth.ut index 121e4add..a9ce7af3 100644 --- a/luci-theme-aurora/ucode/template/themes/aurora/sysauth.ut +++ b/luci-theme-aurora/ucode/template/themes/aurora/sysauth.ut @@ -54,12 +54,52 @@ const loginIconVer = themeTokens.icon_cache_version || '0'; + + {% if (auth_fields): %} + {% for (let field in auth_fields): %} + + {% endfor %} + {% endif %} + + {% if (auth_html): %} + + {% endif %} + + {% if (auth_assets): %} + {% for (let asset in auth_assets): %} + {% if (asset.type == 'script'): %} + + {% endif %} + {% endfor %} + {% endif %} - {% if (fuser): %} - + {% if (auth_message || fuser): %} + {% endif %} + + diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index 1ce6232a..2a120f29 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -42,9 +42,11 @@ function run(records) { * document that was never that short. Written before the tick (a pin in the same statement * sequence is never laid out: 1882px still clamped away) and on the containers, never the column, * where it suppresses the engine's own anchoring (css-scroll-anchoring-1 §2.2.2) and cost 120px of - * growth, all 120px. The selector covers what `dom.content()` empties: a section body and a table - * (a table's body climbs to the same box). */ -const SHRINKS = '.cbi-section > div, .table'; + * growth, all 120px. The selector covers what `dom.content()` empties: a section body, a table + * (a table's body climbs to the same box), and luci-mod-dashboard's KPI row, charts and tab map, + * which sit straight under `.Dashboard`. Without their floor WebKit 27.2 put a reader 526px down + * the dashboard at 0 on every poll, at 1280, 820 and 390px alike. */ +const SHRINKS = '.cbi-section > div, .table, .Dashboard > div'; const FLOORED = '[data-fs-floor]'; @@ -149,6 +151,9 @@ function scrollTop() { const sc = scroller(); return sc ? sc.scrollTop : window.scrollY; } +function docHeight() { + return (scroller() || document.documentElement).scrollHeight; +} function scrolling() { return Date.now() < _movingUntil; } function sampleMotion() { @@ -276,7 +281,7 @@ function rememberRest(force) { const ref = anchorRef(); _restAt = scrollTop(); _restPage = pageStamp(); - _rest = ref ? { el: ref.el, top: ref.top, at: _restAt, sec: ref.sec, secTop: ref.secTop } : null; + _rest = ref ? { el: ref.el, top: ref.top, at: _restAt, sec: ref.sec, secTop: ref.secTop, tall: ref.tall } : null; } function anchorFor() { @@ -285,11 +290,11 @@ function anchorFor() { if (!_rest || !_rest.el.isConnected) { if (clamped) return { by: _restAt - at }; if (_rest && _rest.sec && _rest.sec.isConnected && at === _restAt) - return { el: _rest.sec, top: _rest.secTop, slack: 0 }; + return { el: _rest.sec, top: _rest.secTop, slack: 0, tall: _rest.tall }; return anchorRef(); } if (at !== _rest.at && !clamped) return null; - return { el: _rest.el, top: _rest.top, slack: Math.max(0, _rest.at - at) }; + return { el: _rest.el, top: _rest.top, slack: Math.max(0, _rest.at - at), tall: _rest.tall }; } function anchorRef() { @@ -326,7 +331,7 @@ function anchorRef() { keep = keep.parentElement; if (keep === host) keep = null; return { el, top: el.getBoundingClientRect().top, - sec: keep, secTop: keep ? keep.getBoundingClientRect().top : 0 }; + sec: keep, secTop: keep ? keep.getBoundingClientRect().top : 0, tall: docHeight() }; } let _anchorPending = null; @@ -375,7 +380,11 @@ function applyAnchor(ref) { if (!ref.el.isConnected) return; const drift = ref.el.getBoundingClientRect().top - ref.top; if (Math.abs(drift) < 1) return; /* nothing needed correcting here */ - if (Math.abs(drift) > (window.innerHeight || 0) + 200 + (ref.slack || 0)) return; + /* The cap: one viewport, or what the document grew or shrank since `ref`, whichever is more. A + * subtree swap moves a reference by thousands while the height holds; five leases at 390px move it + * 1290px, past an 844px viewport's 1044px, and the document grew all 1290. */ + const grew = Math.abs(docHeight() - ref.tall); + if (Math.abs(drift) > Math.max(window.innerHeight || 0, grew) + 200 + (ref.slack || 0)) return; writeOffset(at + drift); return true; } diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js index d71468ed..6077cbbe 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js @@ -176,6 +176,21 @@ function bodyLittered() { return littered; } +/* ---- a flushed menu makes the next navigation a full load ---- + * luci-app-package-manager calls ui.menu.flushCache() after an install, upgrade or removal. The + * menu tree and every view class this document required may then be stale: a revisit would rerun + * the old module. */ +let _menuFlushed = false; +function watchMenuFlush() { + const menu = ui.menu; + if (!menu || typeof menu.flushCache !== 'function') return; + const flush = menu.flushCache; + menu.flushCache = function () { + _menuFlushed = true; + return flush.apply(this, arguments); + }; +} + let _curPath = window.location.pathname; let _curSearch = window.location.search; let _navGen = 0; @@ -496,6 +511,8 @@ function navigate(pathname, push, kbd) { if (bodyLittered()) return false; + if (_menuFlushed) return false; + /* an extension's lodash/underscore replaced window._: LuCI's own `_('x').format()` then throws on every view; a full load restores it */ if (typeof _ !== 'function' || typeof _('') !== 'string') return false; @@ -798,6 +815,7 @@ document.addEventListener('poll-stop', () => { }); watchSession(); +watchMenuFlush(); const _navCbs = []; function onNavigate(fn) { if (typeof fn === 'function') _navCbs.push(fn); } diff --git a/luci-theme-footstrap/styles/theme/70-modal.css b/luci-theme-footstrap/styles/theme/70-modal.css index 7e7e8271..5514f493 100644 --- a/luci-theme-footstrap/styles/theme/70-modal.css +++ b/luci-theme-footstrap/styles/theme/70-modal.css @@ -16,6 +16,8 @@ /* 600px is cramped for a table form (.cbi-map cards on a big screen); vw widens toward 1100 on desktop only */ .modal:has(.cbi-map) { max-width: min(68.75rem, 94vw); } + /* a command log (Package Manager) wrapped at 70 mono columns in 600px; 900px holds 108 (13px JetBrains Mono) */ + .modal:has(> pre) { max-width: min(56.25rem, 94vw); } /* no HORIZONTAL scroll ever (measured 634px scroll vs 530px room) — docs/css.md "Modal: no horizontal scroll, ever" */ #modal_overlay .modal label.btn { diff --git a/luci-theme-shadcn/.dev/src/media/login.css b/luci-theme-shadcn/.dev/src/media/login.css index 677b74e1..5241e745 100644 --- a/luci-theme-shadcn/.dev/src/media/login.css +++ b/luci-theme-shadcn/.dev/src/media/login.css @@ -140,8 +140,13 @@ html { /* ── Form elements ── */ -.login-error { +.login-error, +.alert-message { @apply border-destructive-a30 bg-destructive-a8 text-destructive mb-4 rounded-2xl border px-4 py-3 text-sm; + + &.warning { + @apply border-warning-a30 bg-warning-a10 text-warning-soft-fg; + } } .login-field { @@ -162,7 +167,11 @@ html { } input { - @apply text-foreground placeholder:text-muted-foreground w-full bg-transparent py-3.5 pr-10 pl-9 text-sm focus:outline-none; + @apply text-foreground placeholder:text-muted-foreground w-full bg-transparent px-4 py-3.5 text-sm focus:outline-none; + } + + .login-input-icon ~ input { + @apply pl-9; } } diff --git a/luci-theme-shadcn/Makefile b/luci-theme-shadcn/Makefile index 8ac3db50..8d70fb86 100644 --- a/luci-theme-shadcn/Makefile +++ b/luci-theme-shadcn/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design language. LUCI_DEPENDS:=+luci-base -PKG_VERSION:=0.6.2 +PKG_VERSION:=0.6.3 PKG_RELEASE:=20261104 PKG_LICENSE:=Apache-2.0 diff --git a/luci-theme-shadcn/htdocs/luci-static/shadcn/login.css b/luci-theme-shadcn/htdocs/luci-static/shadcn/login.css index 40073241..06163344 100644 --- a/luci-theme-shadcn/htdocs/luci-static/shadcn/login.css +++ b/luci-theme-shadcn/htdocs/luci-static/shadcn/login.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-2xl:1rem;--radius-3xl:1.5rem;}}@layer base{*,:before,:after{box-sizing:border-box}body,p{margin:calc(var(--spacing) * 0)}button,input{margin:calc(var(--spacing) * 0);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:inherit;font:inherit;background-color:#0000;border-width:0}::placeholder{opacity:1}[hidden]{display:none!important}}@layer components,utilities;@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-wght-normal.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+1F??}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-wght-normal.woff2)format("woff2-variations");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-vietnamese-wght-normal.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-wght-normal.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--background:oklch(98.5% .004 275);--foreground:oklch(18% .02 275);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--destructive:oklch(55% .2 25);--border:oklch(89.65% .0058 275);--muted-foreground:oklch(52.62% .0131 275);--primary-hover:oklch(62.7% .158 275);--primary-a10:oklch(56.7% .158 275/.1);--primary-a20:oklch(56.7% .158 275/.2);--primary-a70:oklch(56.7% .158 275/.7);--destructive-a8:oklch(55% .2 25/.08);--destructive-a30:oklch(55% .2 25/.3);--foreground-a5:oklch(18% .02 275/.05);--font-sans:"Inter Variable", ui-sans-serif, system-ui, sans-serif;}[data-darkmode=true]{--background:oklch(15.9% .005 264);--foreground:oklch(90% .006 264);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--destructive:oklch(63% .16 25);--border:oklch(26.27% .0051 264);--muted-foreground:oklch(58.14% .0056 264);--primary-hover:oklch(62.7% .158 275);--primary-a10:oklch(56.7% .158 275/.1);--primary-a20:oklch(56.7% .158 275/.2);--primary-a70:oklch(56.7% .158 275/.7);--destructive-a8:oklch(63% .16 25/.08);--destructive-a30:oklch(63% .16 25/.3);--foreground-a5:oklch(90% .006 264/.05);}html{color-scheme:light}html[data-darkmode=true]{color-scheme:dark}.card-header img,.login-input-icon img,.login-pw-toggle img,img.shadcn-icon{filter:brightness(0)saturate()}html[data-darkmode=true] .card-header img,html[data-darkmode=true] .login-input-icon img,html[data-darkmode=true] .login-pw-toggle img,html[data-darkmode=true] img.shadcn-icon{filter:brightness(0)saturate()invert()}.card-header img,img.shadcn-icon,#sidebar img.shadcn-icon{opacity:.65}html[data-darkmode=true] #sidebar img.shadcn-icon{opacity:.7}.card-header button:hover img,#sidebar .sidebar-nav-item.active img,#sidebar .sidebar-accordion-item.active img,#sidebar .sidebar-nav-parent:hover img{opacity:1}html{background-color:var(--background);font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}@keyframes fadeSlideIn{to{opacity:1;transform:translateY(0)}}@keyframes slideRightIn{to{opacity:1;transform:translate(0)}}.animate-element{opacity:0;animation:.35s ease-out forwards fadeSlideIn;transform:translateY(8px)}.animate-slide-right{opacity:0;animation:.4s ease-out forwards slideRightIn;transform:translate(24px)}.animate-delay-100{animation-delay:.1s}.animate-delay-200{animation-delay:.2s}.animate-delay-300{animation-delay:.3s}.animate-delay-400{animation-delay:.4s}@media (prefers-reduced-motion:reduce){.animate-element,.animate-slide-right{opacity:1;animation:none;transform:none}}.login-layout{min-height:100vh;display:flex}.login-form-panel{background-color:var(--background);padding-inline:calc(var(--spacing) * 8);padding-top:calc(var(--spacing) * 12);padding-bottom:calc(var(--spacing) * 16);flex-direction:column;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.login-form-wrap{width:100%;max-width:var(--container-md)}.login-brand{margin-bottom:calc(var(--spacing) * 10);gap:calc(var(--spacing) * 2.5);flex-direction:column;display:flex}.login-logo{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--primary-a20);background-color:var(--primary-a10);flex-shrink:0;justify-content:center;align-items:center;display:flex;overflow:hidden}.login-logo img{width:100%;height:100%}.login-logo--topleft{top:calc(var(--spacing) * 8);left:calc(var(--spacing) * 8);position:absolute}.login-hostname{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);color:var(--foreground)}.login-subtitle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground)}.login-error{margin-bottom:calc(var(--spacing) * 4);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--destructive-a30);background-color:var(--destructive-a8);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--destructive)}.login-field{margin-bottom:calc(var(--spacing) * 6)}.login-label{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));font-weight:var(--font-weight-medium);color:var(--muted-foreground);display:block}.login-input-glass{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--foreground-a5);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);transition:color .15s,background-color .15s,border-color .15s;position:relative}.login-input-glass:focus-within{border-color:var(--primary-a70);background-color:var(--primary-a10)}.login-input-glass input{width:100%;padding-block:calc(var(--spacing) * 3.5);padding-right:calc(var(--spacing) * 10);padding-left:calc(var(--spacing) * 9);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);background-color:#0000}.login-input-glass input::placeholder{color:var(--muted-foreground)}.login-input-glass input:focus{outline-style:none}.login-input-icon{pointer-events:none;inset-block:calc(var(--spacing) * 0);left:calc(var(--spacing) * 3);align-items:center;display:flex;position:absolute}.login-input-icon img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);opacity:.4}.login-pw-toggle{inset-block:calc(var(--spacing) * 0);right:calc(var(--spacing) * 0);cursor:pointer;border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);background-color:#0000;border-width:0;align-items:center;display:flex;position:absolute}.login-pw-toggle img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);opacity:.4;transition:opacity .15s}.login-pw-toggle:hover img{opacity:.7}.login-submit{margin-top:calc(var(--spacing) * 4);cursor:pointer;border-radius:var(--radius-2xl);border-style:var(--tw-border-style);background-color:var(--primary);width:100%;padding-block:calc(var(--spacing) * 3.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));font-weight:var(--font-weight-medium);color:var(--primary-foreground);border-width:0;transition:color .15s,background-color .15s,border-color .15s}@media (hover:hover){.login-submit:hover{background-color:var(--primary-hover)}}.login-footer{right:calc(var(--spacing) * 0);bottom:calc(var(--spacing) * 5);left:calc(var(--spacing) * 0);padding-inline:calc(var(--spacing) * 6);text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);position:absolute}.login-footer a{color:var(--muted-foreground);text-underline-offset:2px;text-decoration-line:underline;transition:color .15s,background-color .15s,border-color .15s}@media (hover:hover){.login-footer a:hover{color:var(--foreground)}}.login-image-panel{padding:calc(var(--spacing) * 4);display:none;position:relative}@media (width>=64rem){.login-image-panel{flex:1;display:block}}.login-image-inner{inset:calc(var(--spacing) * 4);border-radius:var(--radius-3xl);background-position:50%;background-size:cover;background-image:var(--login-bg-image,linear-gradient(160deg, oklch(18% .12 255) 0%, oklch(10% .04 270) 100%));position:absolute}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-2xl:1rem;--radius-3xl:1.5rem;}}@layer base{*,:before,:after{box-sizing:border-box}body,p{margin:calc(var(--spacing) * 0)}button,input{margin:calc(var(--spacing) * 0);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:inherit;font:inherit;background-color:#0000;border-width:0}::placeholder{opacity:1}[hidden]{display:none!important}}@layer components,utilities;@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-wght-normal.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+1F??}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-wght-normal.woff2)format("woff2-variations");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-vietnamese-wght-normal.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-wght-normal.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--background:oklch(98.5% .004 275);--foreground:oklch(18% .02 275);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--destructive:oklch(55% .2 25);--border:oklch(89.65% .0058 275);--muted-foreground:oklch(52.62% .0131 275);--primary-hover:oklch(62.7% .158 275);--warning-soft-fg:oklch(42% .12 75);--primary-a10:oklch(56.7% .158 275/.1);--primary-a20:oklch(56.7% .158 275/.2);--primary-a70:oklch(56.7% .158 275/.7);--destructive-a8:oklch(55% .2 25/.08);--destructive-a30:oklch(55% .2 25/.3);--foreground-a5:oklch(18% .02 275/.05);--warning-a10:oklch(70% .13 75/.1);--warning-a30:oklch(70% .13 75/.3);--font-sans:"Inter Variable", ui-sans-serif, system-ui, sans-serif;}[data-darkmode=true]{--background:oklch(15.9% .005 264);--foreground:oklch(90% .006 264);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--destructive:oklch(63% .16 25);--border:oklch(26.27% .0051 264);--muted-foreground:oklch(58.14% .0056 264);--primary-hover:oklch(62.7% .158 275);--warning-soft-fg:oklch(86% .1 85);--primary-a10:oklch(56.7% .158 275/.1);--primary-a20:oklch(56.7% .158 275/.2);--primary-a70:oklch(56.7% .158 275/.7);--destructive-a8:oklch(63% .16 25/.08);--destructive-a30:oklch(63% .16 25/.3);--foreground-a5:oklch(90% .006 264/.05);--warning-a10:oklch(80% .12 80/.1);--warning-a30:oklch(80% .12 80/.3);}html{color-scheme:light}html[data-darkmode=true]{color-scheme:dark}.card-header img,.login-input-icon img,.login-pw-toggle img,img.shadcn-icon{filter:brightness(0)saturate()}html[data-darkmode=true] .card-header img,html[data-darkmode=true] .login-input-icon img,html[data-darkmode=true] .login-pw-toggle img,html[data-darkmode=true] img.shadcn-icon{filter:brightness(0)saturate()invert()}.card-header img,img.shadcn-icon,#sidebar img.shadcn-icon{opacity:.65}html[data-darkmode=true] #sidebar img.shadcn-icon{opacity:.7}.card-header button:hover img,#sidebar .sidebar-nav-item.active img,#sidebar .sidebar-accordion-item.active img,#sidebar .sidebar-nav-parent:hover img{opacity:1}html{background-color:var(--background);font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}@keyframes fadeSlideIn{to{opacity:1;transform:translateY(0)}}@keyframes slideRightIn{to{opacity:1;transform:translate(0)}}.animate-element{opacity:0;animation:.35s ease-out forwards fadeSlideIn;transform:translateY(8px)}.animate-slide-right{opacity:0;animation:.4s ease-out forwards slideRightIn;transform:translate(24px)}.animate-delay-100{animation-delay:.1s}.animate-delay-200{animation-delay:.2s}.animate-delay-300{animation-delay:.3s}.animate-delay-400{animation-delay:.4s}@media (prefers-reduced-motion:reduce){.animate-element,.animate-slide-right{opacity:1;animation:none;transform:none}}.login-layout{min-height:100vh;display:flex}.login-form-panel{background-color:var(--background);padding-inline:calc(var(--spacing) * 8);padding-top:calc(var(--spacing) * 12);padding-bottom:calc(var(--spacing) * 16);flex-direction:column;flex:1;justify-content:center;align-items:center;display:flex;position:relative}.login-form-wrap{width:100%;max-width:var(--container-md)}.login-brand{margin-bottom:calc(var(--spacing) * 10);gap:calc(var(--spacing) * 2.5);flex-direction:column;display:flex}.login-logo{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--primary-a20);background-color:var(--primary-a10);flex-shrink:0;justify-content:center;align-items:center;display:flex;overflow:hidden}.login-logo img{width:100%;height:100%}.login-logo--topleft{top:calc(var(--spacing) * 8);left:calc(var(--spacing) * 8);position:absolute}.login-hostname{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);color:var(--foreground)}.login-subtitle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground)}.login-error,.alert-message{margin-bottom:calc(var(--spacing) * 4);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--destructive-a30);background-color:var(--destructive-a8);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--destructive)}:is(.login-error,.alert-message).warning{border-color:var(--warning-a30);background-color:var(--warning-a10);color:var(--warning-soft-fg)}.login-field{margin-bottom:calc(var(--spacing) * 6)}.login-label{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));font-weight:var(--font-weight-medium);color:var(--muted-foreground);display:block}.login-input-glass{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--foreground-a5);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);transition:color .15s,background-color .15s,border-color .15s;position:relative}.login-input-glass:focus-within{border-color:var(--primary-a70);background-color:var(--primary-a10)}.login-input-glass input{width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);background-color:#0000}.login-input-glass input::placeholder{color:var(--muted-foreground)}.login-input-glass input:focus{outline-style:none}.login-input-glass .login-input-icon~input{padding-left:calc(var(--spacing) * 9)}.login-input-icon{pointer-events:none;inset-block:calc(var(--spacing) * 0);left:calc(var(--spacing) * 3);align-items:center;display:flex;position:absolute}.login-input-icon img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);opacity:.4}.login-pw-toggle{inset-block:calc(var(--spacing) * 0);right:calc(var(--spacing) * 0);cursor:pointer;border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);background-color:#0000;border-width:0;align-items:center;display:flex;position:absolute}.login-pw-toggle img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);opacity:.4;transition:opacity .15s}.login-pw-toggle:hover img{opacity:.7}.login-submit{margin-top:calc(var(--spacing) * 4);cursor:pointer;border-radius:var(--radius-2xl);border-style:var(--tw-border-style);background-color:var(--primary);width:100%;padding-block:calc(var(--spacing) * 3.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));font-weight:var(--font-weight-medium);color:var(--primary-foreground);border-width:0;transition:color .15s,background-color .15s,border-color .15s}@media (hover:hover){.login-submit:hover{background-color:var(--primary-hover)}}.login-footer{right:calc(var(--spacing) * 0);bottom:calc(var(--spacing) * 5);left:calc(var(--spacing) * 0);padding-inline:calc(var(--spacing) * 6);text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);position:absolute}.login-footer a{color:var(--muted-foreground);text-underline-offset:2px;text-decoration-line:underline;transition:color .15s,background-color .15s,border-color .15s}@media (hover:hover){.login-footer a:hover{color:var(--foreground)}}.login-image-panel{padding:calc(var(--spacing) * 4);display:none;position:relative}@media (width>=64rem){.login-image-panel{flex:1;display:block}}.login-image-inner{inset:calc(var(--spacing) * 4);border-radius:var(--radius-3xl);background-position:50%;background-size:cover;background-image:var(--login-bg-image,linear-gradient(160deg, oklch(18% .12 255) 0%, oklch(10% .04 270) 100%));position:absolute}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid} diff --git a/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut b/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut index a4526ef7..f67d498f 100644 --- a/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut +++ b/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut @@ -8,8 +8,8 @@ import { getuid, getspnam } from 'luci.core'; import { lsdir } from 'fs'; - const boardinfo = ubus.call('system', 'board') ?? {}; - const rootsp = getspnam('root'); + const boardinfo = prefetched_boardinfo ?? (ubus.call('system', 'board') ?? {}); + const rootsp = !blank_page ? getspnam('root') : null; const root_pw_unset = (rootsp && rootsp.pwdp == ''); const page_segs = length(ctx?.request_path) ? ctx.request_path : (ctx?.path || []); @@ -82,8 +82,10 @@ {% if (css): %} {% endif %} + {% if (!blank_page): %} + {% endif %} diff --git a/luci-theme-shadcn/ucode/template/themes/shadcn/sysauth.ut b/luci-theme-shadcn/ucode/template/themes/shadcn/sysauth.ut index b5aa8610..a55ba72b 100644 --- a/luci-theme-shadcn/ucode/template/themes/shadcn/sysauth.ut +++ b/luci-theme-shadcn/ucode/template/themes/shadcn/sysauth.ut @@ -4,8 +4,8 @@ Licensed to the public under the Apache License 2.0. -#} -{%const boardinfo = ubus.call('system', 'board') ?? {};-%} -{% include('header', { blank_page: true }) %} +{% const boardinfo = ubus.call('system', 'board') ?? {}; -%} +{% include('header', { blank_page: true, prefetched_boardinfo: boardinfo }) %}
@@ -24,9 +24,9 @@