update 2026-11-04 23:55:01
marry-jell / merge (push) Waiting to run

This commit is contained in:
action committed 2026-11-04 23:55:01 +08:00
1 parent b1cd3aafd3
commit 3cdb943a21
25 files changed
+614 -77

No files matched your search

+6 -6
View File
@@ -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.
+246 -14
View File
@@ -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
+14 -2
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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;
@@ -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;
@@ -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,
+94 -20
View File
@@ -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 "切换为面板管理"
+9 -2
View File
@@ -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
;;
@@ -81,7 +81,8 @@
"update_apply",
"config_read",
"config_draft_read",
"config_apply"
"config_apply",
"config_detach"
]
},
"file": {
+1 -1
View File
@@ -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 \
@@ -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 {
@@ -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.
+1 -1
View File
@@ -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
@@ -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-filLine truncated
@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:Line truncated
@@ -54,12 +54,52 @@ const loginIconVer = themeTokens.icon_cache_version || '0';
<input name="luci_password" id="luci_password" type="password" class="login-input" autocomplete="current-password">
</div>
</div>
{% if (auth_fields): %}
{% for (let field in auth_fields): %}
<div class="login-field">
<label class="login-label" for="{{ field.name }}">{{ _(field.label ?? field.name) }}</label>
<div class="input-wrap">
<input
name="{{ field.name }}"
id="{{ field.name }}"
type="{{ field.type ?? 'text' }}"
class="login-input"
{% if (field.placeholder): %}placeholder="{{ field.placeholder }}"{% endif %}
{% if (field.inputmode): %}inputmode="{{ field.inputmode }}"{% endif %}
{% if (field.pattern): %}pattern="{{ field.pattern }}"{% endif %}
{% if (field.maxlength): %}maxlength="{{ field.maxlength }}"{% endif %}
{% if (field.autocomplete): %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if (field.required): %}required{% endif %}
>
</div>
</div>
{% endfor %}
{% endif %}
{% if (auth_html): %}
<div class="login-field">
{{ auth_html }}
</div>
{% endif %}
{% if (auth_assets): %}
{% for (let asset in auth_assets): %}
{% if (asset.type == 'script'): %}
<script src="{{ asset.src }}"></script>
{% endif %}
{% endfor %}
{% endif %}
</div>
{% if (fuser): %}
<div class="login-error" role="alert">{{ _('Invalid username and/or password! Please try again.') }}</div>
{% if (auth_message || fuser): %}
<div class="login-error{% if (auth_plugin): %} warning{% endif %}" role="alert">
{{ auth_message || _('Invalid username and/or password! Please try again.') }}
</div>
{% endif %}
<input type="hidden" name="luci_magic" value="{{ luci_magic }}">
<div class="login-actions">
<button type="submit" class="login-submit">{{ _('Log in') }}</button>
</div>
@@ -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;
}
@@ -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); }
@@ -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 {
+11 -2
View File
@@ -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;
}
}
+1 -1
View File
@@ -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
@@ -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;transfLine truncated
@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);Line truncated
@@ -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): %}
<style data-luci-shell>{{ css }}</style>
{% endif %}
{% if (!blank_page): %}
<script src="{{ dispatcher.build_url('admin/translations', dispatcher.lang) }}"></script>
<script src="{{ resource }}/cbi.js"></script>
{% endif %}
</head>
<body class="lang_{{ dispatcher.lang }}" data-page="{{ entityencode(page, true) }}" data-asset-version="{# PKG_VERSION #}"{% if (!blank_page): %} data-nav-stamp="{{ entityencode(join(':', [dispatcher.lang, ctx?.authuser ?? '', ctx?.authsession ?? '']), true) }}:{# PKG_VERSION #}"{% endif %}{% if (length(patch_all)): %} data-patches="{{ entityencode(join(' ', patch_all), true) }}"{% endif %}>
@@ -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 }) %}
<style>:root{--login-bg-image:url('{{ media }}/images/login-bg.jpeg')}</style>
<main class="login-layout">
@@ -24,9 +24,9 @@
</div>
<form method="post" class="login-form">
{% if (fuser): %}
<div class="login-error animate-element animate-delay-200">
{{ _('Invalid username and/or password! Please try again.') }}
{% if (auth_message || fuser): %}
<div class="login-error{% if (auth_plugin): %} warning{% endif %} animate-element animate-delay-200">
{{ auth_message || _('Invalid username and/or password! Please try again.') }}
</div>
{% endif %}
@@ -42,7 +42,8 @@
id="luci_username"
placeholder="{{ _('Username') }}"
autocomplete="username"
{% if (duser): %}value="{{ entityencode(duser, true) }}"{% else %}value=""{% endif %}
value="{{ entityencode(duser ?? '', true) }}"
{% if (!duser): %}autofocus{% endif %}
>
</div>
</div>
@@ -59,7 +60,7 @@
id="luci_password"
placeholder="{{ _('Password') }}"
autocomplete="current-password"
autofocus
{% if (duser): %}autofocus{% endif %}
>
<button type="button" class="login-pw-toggle" id="pw-toggle" aria-label="{{ _('Reveal/hide password') }}">
<img src="{{ media }}/icons/eye.svg" id="pw-toggle-icon" width="16" height="16" alt="">
@@ -67,6 +68,39 @@
</div>
</div>
{% if (auth_fields): %}
{% for (let field in auth_fields): %}
<div class="login-field animate-element animate-delay-300">
<label class="login-label" for="{{ field.name }}">{{ _(field.label ?? field.name) }}</label>
<div class="login-input-glass">
<input
name="{{ field.name }}"
id="{{ field.name }}"
type="{{ field.type ?? 'text' }}"
{% if (field.placeholder): %}placeholder="{{ field.placeholder }}"{% endif %}
{% if (field.inputmode): %}inputmode="{{ field.inputmode }}"{% endif %}
{% if (field.pattern): %}pattern="{{ field.pattern }}"{% endif %}
{% if (field.maxlength): %}maxlength="{{ field.maxlength }}"{% endif %}
{% if (field.autocomplete): %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if (field.required): %}required{% endif %}
>
</div>
</div>
{% endfor %}
{% endif %}
{% if (auth_html): %}
<div class="login-field animate-element animate-delay-300">
{{ auth_html }}
</div>
{% endif %}
{% if (auth_assets): %}
{% for (let asset in auth_assets): %}
{% if (asset.type == 'script'): %}
<script src="{{ asset.src }}"></script>
{% endif %}
{% endfor %}
{% endif %}
<input type="hidden" name="luci_magic" value="{{ luci_magic }}">
<button type="submit" class="login-submit animate-element animate-delay-400">
+2 -2
View File
@@ -21,13 +21,13 @@ define Download/geoip
HASH:=eedb24b3b9fcba27e06b92c0428dab90d9496083091ae4cf39e11387fd99d0e2
endef
GEOSITE_VER:=20261103223032
GEOSITE_VER:=20261104112839
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
HASH:=9c1b21ba6aef9d9a077642bf6bed52d736dce68eb46865bc9867f8eb73c4a410
HASH:=72be4fe4e4045467b982808520051bb1d4af325af290e89999ffdea7aceec131
endef
GEOSITE_IRAN_VER:=202610230802