🔥 Sync 2026-09-05 21:18:46

This commit is contained in:
github-actions[bot]
2026-09-05 21:18:46 +08:00
parent 28a85181b8
commit 01aa5b5f9e
45 changed files with 506 additions and 272 deletions
+15 -2
View File
@@ -15,7 +15,7 @@ AGENTFLOW_HASH_aarch64:=6d733ebc49b095bf8670a1b655a13bb8ad98c90ced19701b67d5cdd4
PKG_NAME:=agentflow
PKG_VERSION:=0.3.0
PKG_RELEASE:=6
PKG_RELEASE:=7
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
@@ -52,6 +52,16 @@ define Package/$(PKG_NAME)/conffiles
/etc/config/agentflow
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ "$${IPKG_INSTROOT}" = "" ]; then
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/30-agentflow.json 2>/dev/null)" = "/usr/share/agentflow/agentflow-plugin.json" ]; then
rm -f /usr/share/linkeasefull/desktop-apps.d/30-agentflow.json
fi
fi
exit 0
endef
define Build/Prepare
$(INSTALL_DIR) $(PKG_BUILD_DIR)
$(CP) $(DL_DIR)/$(AGENTFLOW_DOWNLOAD) $(PKG_BUILD_DIR)/agentflow
@@ -64,11 +74,14 @@ define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config $(1)/usr/share/agentflow/www $(1)/usr/share/linkeasefull/desktop-apps.d
$(INSTALL_BIN) $(PKG_BUILD_DIR)/agentflow $(1)/usr/sbin/agentflow
$(INSTALL_BIN) ./files/agentflow.init $(1)/etc/init.d/agentflow
$(INSTALL_BIN) ./files/agentflow.uci-default $(1)/etc/uci-defaults/09-agentflow
$(INSTALL_CONF) ./files/agentflow.config $(1)/etc/config/agentflow
$(INSTALL_DATA) ./files/agentflow-plugin.json $(1)/usr/share/agentflow/agentflow-plugin.json
$(INSTALL_DATA) ./files/www/logo.svg $(1)/usr/share/agentflow/www/logo.svg
ln -sf /usr/share/agentflow/agentflow-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/30-agentflow.json
endef
$(eval $(call Download,agentflow))
+41
View File
@@ -0,0 +1,41 @@
{
"schemaVersion": 1,
"id": "agentflow",
"name": "AgentFlow",
"icon": "logo.svg",
"staticRoot": "/usr/share/agentflow/www",
"desktop": {
"mode": "module",
"entry": "desktop-entry.js",
"isolation": "shadow-dom"
},
"standalone": {
"entry": "index.html",
"basePath": "/apps/agentflow/",
"url": "/cgi-bin/luci/admin/services/agentflow/open",
"externalOpen": {
"enabled": true,
"label": "Open AgentFlow"
}
},
"auth": {
"mode": "passthrough"
},
"backend": {
"type": "http",
"scheme": "http",
"host": "127.0.0.1",
"portFromUci": "agentflow.@agentflow[0].port",
"defaultPort": 9000,
"upstreamBasePath": "/apps/agentflow/",
"pathMode": "preserve",
"proxyMode": "app-base"
},
"window": {
"width": 1180,
"height": 760,
"singleton": true
},
"capabilities": ["agent", "coding", "workflow"],
"categories": ["development", "ai"]
}
+21
View File
@@ -0,0 +1,21 @@
<svg
width="150"
height="150"
viewBox="0 0 150 150"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="150" height="150" rx="30" fill="#f5f1e8" />
<text
x="50%"
y="54%"
text-anchor="middle"
dominant-baseline="middle"
font-size="68"
font-weight="700"
font-family="Georgia, 'Times New Roman', serif"
letter-spacing="-4"
fill="#111111"
>
AF
</text>
</svg>

After

Width:  |  Height:  |  Size: 408 B

@@ -0,0 +1,48 @@
import json
import pathlib
import unittest
APP_DIR = pathlib.Path(__file__).resolve().parents[1]
class DesktopManifestContractTest(unittest.TestCase):
def setUp(self):
self.manifest = json.loads(
(APP_DIR / "files" / "agentflow-plugin.json").read_text()
)
def test_manifest_uses_agentflow_app_base_proxy(self):
self.assertEqual(self.manifest["id"], "agentflow")
self.assertEqual(self.manifest["staticRoot"], "/usr/share/agentflow/www")
self.assertEqual(self.manifest["standalone"]["entry"], "index.html")
self.assertEqual(self.manifest["standalone"]["basePath"], "/apps/agentflow/")
self.assertEqual(
self.manifest["standalone"]["url"],
"/cgi-bin/luci/admin/services/agentflow/open",
)
self.assertEqual(
self.manifest["standalone"]["externalOpen"],
{"enabled": True, "label": "Open AgentFlow"},
)
self.assertEqual(self.manifest["auth"]["mode"], "passthrough")
backend = self.manifest["backend"]
self.assertEqual(backend["upstreamBasePath"], "/apps/agentflow/")
self.assertEqual(backend["pathMode"], "preserve")
self.assertEqual(backend["proxyMode"], "app-base")
def test_desktop_entry_matches_manifest_contract(self):
desktop = self.manifest["desktop"]
self.assertEqual(desktop["mode"], "module")
self.assertEqual(desktop["entry"], "desktop-entry.js")
self.assertEqual(desktop["isolation"], "shadow-dom")
self.assertFalse(
(APP_DIR / "files" / "www" / "desktop-entry.js").exists(),
"desktop-entry.js must be served by AgentFlow backend, not the static iframe wrapper",
)
if __name__ == "__main__":
unittest.main()
+18 -3
View File
@@ -2,12 +2,27 @@ from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[3]
APPLICATIONS = ROOT / "applications"
THIS_FILE = Path(__file__).resolve()
def read(relative):
return (APPLICATIONS / relative).read_text()
relative_path = Path(relative)
package = relative_path.parts[0]
app = package[9:] if package.startswith("luci-app-") else package
remainder = Path(*relative_path.parts[1:])
candidates = [
THIS_FILE.parents[index] / relative_path
for index in range(2, min(len(THIS_FILE.parents), 6))
]
candidates.append(THIS_FILE.parents[3] / app / package / remainder)
for candidate in candidates:
if candidate.exists():
return candidate.read_text()
searched = "\n".join(str(candidate) for candidate in candidates)
raise FileNotFoundError(f"{relative} not found; searched:\n{searched}")
class MiseRuntimeContractTest(unittest.TestCase):
+49 -40
View File
@@ -1,47 +1,46 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2017-2024
#
# This is free software, licensed under the GNU General Public License v2.
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser
PKG_VERSION:=1.5.6-stable
PKG_RELEASE=1
PKG_VERSION:=2.63.23
PKG_RELEASE:=21
ifeq ($(ARCH),aarch64)
PKG_ARCH:=arm64
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
PKG_ARCH:=armv7
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_ARCH:=amd64
PKG_HASH:=skip
endif
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_BUILD_DEPENDS:=golang/host node/host node-pnpm/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/filebrowser/filebrowser/v2
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/version.CommitSHA=$(PKG_VERSION) \
$(GO_PKG)/version.Version=v$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/filebrowser
SECTION:=net
CATEGORY:=Network
DEPENDS:=@(arm||aarch64||x86_64)
TITLE:=FileBrowser Quantum
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Web File Browser
URL:=https://github.com/filebrowser/filebrowser
DEPENDS:=$(GO_ARCH_DEPENDS)
endef
define Package/filebrowser/description
The best free self-hosted web-based file manager.
endef
PKG_SOURCE:=linux-$(PKG_ARCH)-filebrowser
PKG_SOURCE_URL:=https://github.com/gtsteffaniak/filebrowser/releases/download/v$(PKG_VERSION)
define Build/Prepare
$(call Build/Prepare/Default)
endef
define Build/Compile
filebrowser provides a file managing interface within a specified directory
and it can be used to upload, delete, preview, rename and edit your files.
It allows the creation of multiple users and each user can have its own directory.
It can be used as a standalone app or as a middleware.
endef
define Package/filebrowser/conffiles
@@ -49,15 +48,25 @@ define Package/filebrowser/conffiles
/etc/config/filebrowser
endef
define Package/filebrowser/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser
$(INSTALL_DIR) $(1)/etc/filebrowser
$(INSTALL_CONF) $(CURDIR)/files/config.yaml $(1)/etc/filebrowser/config.yaml
$(INSTALL_DIR) $(1)/usr/bin/
$(INSTALL_BIN) $(DL_DIR)/linux-$(PKG_ARCH)-filebrowser $(1)/usr/bin/filebrowser
define Build/Compile
( \
export PNPM_HOME="$(PKG_BUILD_DIR)/frontend" ; \
pushd "$(PKG_BUILD_DIR)/frontend" ; \
pnpm install --frozen-lockfile ; \
pnpm run build ; \
popd ; \
$(call GoPackage/Build/Compile) ; \
)
endef
define Package/filebrowser/install
$(call GoPackage/Package/Install/Bin,$(1))
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser
endef
$(eval $(call GoBinPackage,filebrowser))
$(eval $(call BuildPackage,filebrowser))
-13
View File
@@ -1,13 +0,0 @@
server:
port: 8989
database: "/etc/filebrowser/database.db"
sources:
- path: "/" # Do not use a root "/" directory or include the "/var" folder
config:
defaultEnabled: true
auth:
adminUsername: admin
adminPassword: "admin"
userDefaults:
ui:
locale: "cn"
+2
View File
@@ -2,4 +2,6 @@ config filebrowser 'config'
option enabled '0'
option listen_port '8989'
option root_path '/'
option base_url ''
option disable_exec '1'
+32 -15
View File
@@ -1,33 +1,50 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
STOP=10
CONF="filebrowser"
PROG="/usr/bin/filebrowser"
CONF_PATH="/etc/filebrowser/config.yaml"
PID_FILE="/var/run/filebrowser.pid"
DB_PATH="/etc/filebrowser/database.db"
start() {
start_service() {
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
[ "$enabled" -eq "1" ] || return 1
local listen_port root_path base_url
mkdir -p "${DB_PATH%/*}"
local listen_port root_path base_url disable_exec
config_get listen_port "config" "listen_port" "8989"
config_get root_path "config" "root_path" "/"
config_get root_path "config" "root_path" "/mnt/"
config_get base_url "config" "base_url"
config_get_bool disable_exec "config" "disable_exec" "1"
procd_open_instance
procd_set_param command "$PROG"
procd_append_param command --database "$DB_PATH"
procd_append_param command --address "[::]"
procd_append_param command --port "$listen_port"
procd_append_param command --root "$root_path"
procd_append_param command --disable-exec="$disable_exec"
[ -z "$base_url" ] || procd_append_param command --baseurl "$base_url"
if [ ! -e "$DB_PATH" ]; then
procd_append_param command --username "admin"
procd_append_param command --password "$("$PROG" hash "admin")"
fi
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
echo "Starting filebrowser..."
sed -e "s/ port:.*/ port: $listen_port/" \
-e "s, - path:.*, - path: \"$root_path\"," \
-i "$CONF_PATH"
start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH"
}
stop() {
kill -9 `pidof filebrowser | sed "s/$$//g"` 2>/dev/null
rm -f "$PID_FILE"
echo "filebrowser stopped"
service_triggers() {
procd_add_reload_trigger "$CONF"
}
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease-common-bin
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=24
PKG_RELEASE:=25
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
PKG_SOURCE_VERSION:=7093bc8b705b7b239b8adde8e4d89928157de456
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=27
PKG_RELEASE:=28
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
PKG_SOURCE_VERSION:=7093bc8b705b7b239b8adde8e4d89928157de456
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.17
PKG_RELEASE:=26
PKG_RELEASE:=27
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
PKG_HASH:=skip
endif
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
PKG_SOURCE_VERSION:=7093bc8b705b7b239b8adde8e4d89928157de456
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
PKG_VERSION:=1.0.0
PKG_RELEASE:=3
PKG_RELEASE:=4
LUCI_DEPENDS:=+agentflow +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
@@ -2,28 +2,128 @@ local http = require "luci.http"
module("luci.controller.agentflow", package.seeall)
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
local DEFAULT_BASE_PATH = "/apps/agentflow/"
local DEFAULT_PORT = 9000
function index()
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
local open = entry({"admin", "services", "agentflow", "open"}, call("agentflow_open"))
open.leaf = true
open.dependent = false
open.sysauth = false
if not nixio.fs.access("/etc/config/agentflow") then
return
end
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
page.dependent = true
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
end
local function uhttpd_has_apps_proxy_prefix()
local uci = require "luci.model.uci".cursor()
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
for _, mapping in ipairs(mappings) do
if mapping == APPS_PROXY_PREFIX then
return true
end
end
return false
end
local function uhttpd_supports_proxy_prefix()
local sys = require "luci.sys"
return sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
end
local function uhttpd_apps_proxy_available()
return uhttpd_supports_proxy_prefix() and uhttpd_has_apps_proxy_prefix()
end
local function linkeasefull_running()
local sys = require "luci.sys"
return sys.call("[ -x /etc/init.d/linkeasefull ] && /etc/init.d/linkeasefull running >/dev/null 2>&1") == 0
end
local function normalized_base_path(path)
path = path or DEFAULT_BASE_PATH
if path:sub(1, 1) ~= "/" then
path = "/" .. path
end
if path:sub(-1) ~= "/" then
path = path .. "/"
end
return path
end
local function authority_host(authority)
if not authority or authority == "" then
return ""
end
if authority:sub(1, 1) == "[" then
return authority:match("^%[([^%]]+)%]") or ""
end
return authority:match("^([^:]+)") or authority
end
local function url_authority(host, port)
if not host or host == "" then
host = "127.0.0.1"
end
if host:find(":") and host:sub(1, 1) ~= "[" then
host = "[" .. host .. "]"
end
return host .. ":" .. tostring(port)
end
local function request_or_lan_host()
local uci = require "luci.model.uci".cursor()
local host = authority_host(http.getenv("HTTP_HOST") or "")
if host ~= "" then
return host
end
return uci:get("network", "lan", "ipaddr") or "127.0.0.1"
end
local function agentflow_config()
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or DEFAULT_PORT
if port < 1 or port > 65535 then
port = DEFAULT_PORT
end
local base_path = normalized_base_path(uci:get_first("agentflow", "agentflow", "base_path"))
return port, base_path
end
local function agentflow_entry_url()
local port, base_path = agentflow_config()
if linkeasefull_running() and uhttpd_apps_proxy_available() then
return base_path
end
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
end
function agentflow_status()
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or 9000
if port < 1 or port > 65535 then
port = 9000
end
local port, base_path = agentflow_config()
local entry_url = agentflow_entry_url()
local status = {
running = (sys.call("pidof agentflow >/dev/null") == 0),
port = port
port = port,
base_path = base_path,
entry_url = entry_url,
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
linkeasefull_running = linkeasefull_running()
}
http.prepare_content("application/json")
http.write_json(status)
end
function agentflow_open()
local entry_url = agentflow_entry_url()
http.redirect(entry_url)
end
@@ -1,12 +1,20 @@
<script type="text/javascript">//<![CDATA[
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
var el = document.getElementById('agentflow_status');
if (st && el) {
if (!st.running) {
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
} else {
var accessMode = '';
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
} else {
accessMode = '<br/><em><%:Access mode: external port.%></em>';
}
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
+ accessMode
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('" + agentflowOpenUrl + "')\"/>";
}
}
});
+6
View File
@@ -10,6 +10,12 @@ msgstr "AgentFlow 提供用于编排编码智能体和工作流的 Web 管理界
msgid "Click to open AgentFlow"
msgstr "点击打开 AgentFlow"
msgid "Access mode: LinkEase Desktop /apps proxy."
msgstr "访问模式:LinkEase Desktop /apps 代理。"
msgid "Access mode: external port."
msgstr "访问模式:外部端口。"
msgid "Collecting data..."
msgstr "正在获取数据……"
@@ -0,0 +1,32 @@
from pathlib import Path
import unittest
APP_DIR = Path(__file__).resolve().parents[1]
class AgentFlowLuciOpenContractTest(unittest.TestCase):
def read(self, relative):
return (APP_DIR / relative).read_text(encoding="utf-8")
def test_luci_open_matches_linkease_proxy_pattern(self):
makefile = self.read("Makefile")
controller = self.read("luasrc/controller/agentflow.lua")
status = self.read("luasrc/view/agentflow/status.htm")
self.assertNotIn("+luci-lib-linkeaseauth", makefile)
self.assertIn('entry({"admin", "services", "agentflow", "open"}', controller)
self.assertIn("open.sysauth = false", controller)
self.assertIn("function agentflow_open()", controller)
self.assertIn("uhttpd_apps_proxy_available()", controller)
self.assertIn("linkeasefull_running()", controller)
self.assertIn("return base_path", controller)
self.assertIn("url_authority(request_or_lan_host(), port)", controller)
self.assertIn("http.redirect(entry_url)", controller)
self.assertNotIn('linkease_auth_url("auth")', controller)
self.assertIn('url("admin/services/agentflow/open")', status)
self.assertNotIn('window.location.hostname + ":" + st.port', status)
if __name__ == "__main__":
unittest.main()
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
@@ -4,11 +4,22 @@
'require rpc';
'require uci';
'require view';
'require fs';
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList('filebrowser'), {}).then(function(res) {
let isRunning = false;
try {
isRunning = res['filebrowser']['instances']['instance1']['running'];
} catch (e) { }
return isRunning;
});
}
function renderStatus(isRunning, port) {
@@ -26,21 +37,13 @@ function renderStatus(isRunning, port) {
}
return view.extend({
load: async function () {
const promises = await Promise.all([
L.resolveDefault(fs.stat('/var/run/filebrowser.pid'), null),
uci.load('filebrowser')
]);
const data = {
isRunning: promises[0],
conf: promises[1]
};
return data;
load() {
return uci.load('filebrowser');
},
render(data) {
let m, s, o;
let webport = (uci.get(data.conf, 'config', 'listen_port') || '8989');
let webport = (uci.get(data, 'config', 'listen_port') || '8989');
m = new form.Map('filebrowser', _('FileBrowser'),
_('FileBrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files..') + '<br />'+
@@ -49,24 +52,17 @@ return view.extend({
s = m.section(form.TypedSection);
s.anonymous = true;
s.render = function() {
poll.add(function() {
return fs.stat('/var/run/filebrowser.pid').then(function(stat) {
let view = document.getElementById('service_status');
if (view) {
view.innerHTML = renderStatus(stat, webport);
}
}).catch(function() {
let view = document.getElementById('service_status');
if (view) {
view.innerHTML = renderStatus(null, webport);
}
});
});
poll.add(function() {
return L.resolveDefault(getServiceStatus()).then(function(res) {
let view = document.getElementById('service_status');
view.innerHTML = renderStatus(res, webport);
});
});
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
E('p', { id: 'service_status' }, _('Collecting data...'))
]);
};
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
E('p', { id: 'service_status' }, _('Collecting data...'))
]);
}
s = m.section(form.NamedSection, 'config', 'filebrowser');
@@ -80,7 +76,11 @@ return view.extend({
o.rmempty = false;
o = s.option(form.Value, 'root_path', _('Root directory'));
o.default = '/';
o.default = '/mnt';
o.rmempty = false;
o = s.option(form.Flag, 'disable_exec', _('Disable Command Runner feature'));
o.default = o.enabled;
o.rmempty = false;
return m.render();
+1
View File
@@ -0,0 +1 @@
zh_Hans
-1
View File
@@ -1 +0,0 @@
zh_Hans
@@ -1,4 +0,0 @@
{
"config": "filebrowser",
"init": "filebrowser"
}
+1 -1
View File
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI page for IPK upload
LUCI_DEPENDS:=+luci-lib-fs
LUCI_DEPENDS:=+luci-compat
include $(TOPDIR)/feeds/luci/luci.mk
@@ -1,4 +1,4 @@
local fs = require "luci.fs"
local fs = require "nixio.fs"
local http = luci.http
ful = SimpleForm("upload", translate("Upload"), nil)
@@ -28,8 +28,8 @@ dm.template = "filetransfer/other_dvalue"
function Download()
local sPath, sFile, fd, block
sPath = http.formvalue("dlfile")
sFile = nixio.fs.basename(sPath)
if luci.fs.isdirectory(sPath) then
sFile = fs.basename(sPath)
if fs.stat(sPath, "type") == "dir" then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
@@ -121,7 +121,7 @@ btnrm.render = function(self, section, scope)
end
btnrm.write = function(self, section)
local v = luci.fs.unlink("/tmp/upload/" .. luci.fs.basename(inits[section].name))
local v = fs.unlink("/tmp/upload/" .. fs.basename(inits[section].name))
if v then table.remove(inits, section) end
return v
end
+1 -1
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=59
PKG_RELEASE:=60
endif
LUCI_TITLE:=Footstrap Theme
+16 -28
View File
@@ -19,14 +19,9 @@
* friends) live in those blocks too, beside the fills they must be legible against: a
* dark palette has light fills and needs a dark ink, and one global `#fff` failed WCAG AA
* on seven of the eight dark-palette fills (down to 1.69:1). */
--fs-shadow: 0 1px 3px rgba(15,23,37,.08);
/* pop surfaces (menus, dropdown lists, the select picker): one value rather than a
* literal per site */
--fs-shadow-pop: 0 12px 34px rgba(0,0,0,.30);
/* the top bar's edge: a drop below plus a 1px inset seam, both a quiet 25% of the border
* colour */
--fs-shadow-bar: 0 1px 3px color-mix(in srgb, var(--fs-border) 25%, transparent),
inset 0 -1px 0 color-mix(in srgb, var(--fs-border) 25%, transparent);
/* ---- two focus rings, and which one a control takes is the contract ----
* -soft is a TINT (10-15% of the accent), measuring 1.15-1.29:1 on the surface it lands
* on: a glow, not an indicator, where WCAG 1.4.11 wants 3:1. It is legitimate only beside
@@ -79,7 +74,9 @@
* The role x step matrix is complete on purpose, read or not: a gap is how the drift
* starts, since a rule wanting a level that does not exist invents a percentage.
* --fs-accent-soft is the one member not here — its strength differs per MODE (10% light,
* 15% dark; hicontrast 10/14) — so it sits in the palette blocks. */
* 15% dark; hicontrast dark 14%). LIGHT is identical across every palette and lives as one
* declaration in 03-palettes.css's shared :root instead; DARK still varies by palette (one
* differs by a digit) and stays declared per palette block. */
--fs-good-soft: color-mix(in srgb, var(--fs-good) 12%, transparent);
--fs-warn-soft: color-mix(in srgb, var(--fs-warn) 12%, transparent);
--fs-danger-soft: color-mix(in srgb, var(--fs-danger) 12%, transparent);
@@ -103,13 +100,6 @@
* instead of mixing its own percentage. A grep finding no `var(--fs-warn-line)` is
* expected. */
/* frosted pop surface (top-nav dropdown, rail flyout), deliberately not merged into
* --fs-bar-bg (88%): a bar is a thin strip the page ghosts through on purpose, while a
* dropdown carries a whole menu and must stay readable */
--fs-glass: color-mix(in srgb, var(--fs-panel) 96%, transparent);
/* one blur radius for every frosted surface — the bar said 10px and the pops 12px */
--fs-blur: blur(12px);
/* The circular-arrows refresh glyph, as a mask so its colour follows the palette
* (--fs-good active, --fs-dim paused). One copy: the poll pill collapses to it both in the
* rail and in the bar's compact cluster. A static glyph on purpose — a spinner makes an
@@ -127,10 +117,6 @@
* the element and drops the border, the bar keeps the bordered square — but not in size. */
--fs-icon-refresh-sz: calc(18px * var(--fs-density-type));
/* the 1px raised edge on a plate carrying text, plus its text shadow */
--fs-emboss: inset 0 1px 0 color-mix(in srgb, var(--fs-panel) 5%, transparent);
--fs-text-emboss: 0 1px 1px color-mix(in srgb, var(--fs-panel) 75%, transparent);
/* Hover cue for a solid control — the one cue that cannot be a colour, the fill it shifts
* being whatever the role token says. Per mode, and the DIRECTION FLIPS: `filter`
* recolours the text too, and a light-mode solid button is a saturated fill with white
@@ -164,21 +150,24 @@
--fs-sidebar-w: calc(224px * var(--fs-density-box)); /* the expanded vertical sidebar's column */
--fs-rail-w: calc(68px * var(--fs-density-box)); /* ...and the same sidebar collapsed to its icon rail */
--fs-content-min: 500px; /* below this a content column stops being readable */
/* translucent blurred bar surface: the top-nav bar and the sidebar's phone bar are one
* component in two markups, so one token keeps their glass tint identical */
--fs-bar-bg: color-mix(in srgb, var(--fs-panel) 88%, transparent);
/* the bar's surface: the top-nav bar and the sidebar's phone bar are one component in two
* markups, so one token keeps the two identical. OPAQUE on purpose. It was 88% of the panel
* under a 12px backdrop blur, and the pair was measured on the page most favourable to it —
* a pattern wallpaper at full strength, the bar sitting over scrolled content: dropping the
* blur moved 0.04% of the pixels, and what the translucency actually bought was the page
* showing through the chrome, which reads as a glitch rather than as glass. */
--fs-bar-bg: var(--fs-panel);
/* height of that bar, and the scroll-margin every anchor target needs to clear it
* (base/10-reset.css): if the two drift, an in-page jump lands under the bar */
/* The chrome's own controls — the square buttons (search, appearance, log out) and the poll
* pill — ride the bar rather than cutting panel-coloured holes in it: the bar is one surface,
* and a control on it is carried by its hairline, not by a second fill. `transparent`, not a
* copy of --fs-bar-bg, so the strip's own translucency is not applied twice.
* and a control on it is carried by its hairline, not by a second fill. `transparent`, so the
* control shows the bar itself rather than a second surface stacked on it.
*
* This gives up what `.label`'s comment in theme/35-alerts.css leans on — that a coloured
* variant sits on --fs-panel2, where the palette guarantees >=4.5:1 by construction. On the
* bar the backdrop is now whatever the strip lands on, so the ratio is a MEASURED fact per
* palette rather than a guaranteed one; the numbers live in the changelog entry. A palette that
* needs the old behaviour sets this back to var(--fs-panel2). */
* The backdrop a control lands on is therefore --fs-bar-bg, which is --fs-panel: the same
* construction `.label`'s comment in theme/35-alerts.css leans on, so a coloured variant keeps
* its >=4.5:1 by the palette's guarantee rather than by measurement. A palette that wants the
* old panel2 hole sets this back to var(--fs-panel2). */
--fs-bar-ctl-bg: transparent;
/* The select's chevron, drawn rather than fetched. It used to be an inline SVG data-URI, and
* a URI cannot read var() — the stroke colour had to be written INTO the string, so the
@@ -561,7 +550,6 @@
:root[data-darkmode="true"] {
/* palette colours are in 03-palettes.css; only mode-level, palette-independent
* tokens live here */
--fs-shadow: 0 1px 2px rgba(0,0,0,.4);
--fs-disabled-opacity: .45;
/* the hover cue flips direction here (see --fs-hover-lift): a dark-mode solid button is a
* light fill with dark ink, so brightening moves it away from both the page and its own
+48 -51
View File
@@ -65,7 +65,7 @@
* one honest knob --fs-tint-c is the strength, in chroma. Light mode has less headroom and
* does clip at the top of the wheel, which costs a few units of strength
* but not the direction. On a canvas of pure WHITE the cue is nearly
* hue-blind on bootstrap's #ffffff, 180° and 258° land 1/255 apart
* hue-blind on bootstrap's #fff, 180° and 258° land 1/255 apart
* which is the price of copying a stock theme's surfaces.
* contrast is free `l` is copied through, so AA margins barely move; the mix dragged
* lightness toward the anchor and cost one export level 4.61 -> 4.40:1.
@@ -84,6 +84,12 @@
* rotate it with no second copy to keep in sync. Absent an accent hue, --fs-accent IS the
* palette's -base value. */
--fs-accent: var(--fs-accent-base);
/* a tint OF --fs-accent, mixed FROM it: a restated literal goes stale the moment the accent
* is recoloured or the accent-hue axis rotates it. LIGHT strength only every palette's
* light block wanted the identical 10%, so this one declaration replaces five copies; a
* dark block still declares its own (15%, hicontrast 14%: 02-tokens.css line ~81) and wins
* on specificity. */
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
/* the three status colours, derived from the palette's -base hex for the reason the accent
* is: an axis rotating a token the palettes declared directly would need a second copy of
* every value to rotate from */
@@ -253,7 +259,7 @@
:root[data-palette="footstrap"] {
/* LIGHT */
--fs-bg-base: #f6f8fa;
--fs-panel-base: #ffffff;
--fs-panel-base: #fff;
--fs-panel2-base: #f6f8fa;
--fs-border-base: #d0d7de;
--fs-text: #1f2328;
@@ -264,9 +270,6 @@
--fs-dim: #33383d;
--fs-faint: #3d4348;
--fs-accent-base: #0969da;
/* a tint OF --fs-accent, mixed FROM it: a restated literal goes stale the moment the accent
* is recoloured or the accent-hue axis rotates it */
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #eaeef2;
--fs-good-base: #197c36;
--fs-warn-base: #946300;
@@ -311,7 +314,7 @@
* white ink, and both uses want >=4.5:1 against white. At 5.22/5.35/5.93/5.80 they are
* genuinely above the default palette, which is what the name promises. */
--fs-bg-base: #eef1f6;
--fs-panel-base: #ffffff;
--fs-panel-base: #fff;
--fs-panel2-base: #f6f8fb;
--fs-border-base: #e4e9f1;
--fs-text: #101725;
@@ -320,7 +323,6 @@
--fs-dim: #272f3d;
--fs-faint: #2e3745;
--fs-accent-base: #0b6fbd;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #eaeef4;
--fs-good-base: #0f7a52;
--fs-warn-base: #8a5a00;
@@ -360,9 +362,9 @@
* they already know keeps it and still gets the chrome, the client navigation and the axes.
*
* Bootstrap computes everything from HSL axes; these are those axes evaluated:
* --background-color high/medium/low #ffffff / #f9f9f9 / #f5f5f5 (dark #222 / #282828 / #2c2c2c)
* --background-color high/medium/low #fff / #f9f9f9 / #f5f5f5 (dark #222 / #282828 / #2c2c2c)
* --text-color high/medium #404040 / #808080 (dark #bfbfbf / #7f7f7f)
* --border-color high/medium #cccccc / #dddddd (dark #555555 / #444444)
* --border-color high/medium #ccc / #ddd (dark #555 / #444)
* The canvas and the card are both --background-color-high: bootstrap is flat and lets the
* border carry the structure. panel2 is its --background-color-low, the surface it stripes
* tables and hovers rows with.
@@ -381,10 +383,10 @@
* to the chrome, not a colourway. */
:root[data-palette="bootstrap"] {
/* LIGHT */
--fs-bg-base: #ffffff;
--fs-panel-base: #ffffff;
--fs-bg-base: #fff;
--fs-panel-base: #fff;
--fs-panel2-base: #f5f5f5;
--fs-border-base: #cccccc;
--fs-border-base: #ccc;
/* The ink is the one place this colourway leaves bootstrap's own values behind, the same
* decision the default palette made: its #404040 body and #6a6a6a muted tier measure
* 10.4:1 and 5.4:1 on white, and the muted one carries every field title. Matched to the
@@ -395,8 +397,7 @@
--fs-faint: #424242;
/* --primary-color-medium; -high (#1976d2) measured 4.22:1 on --fs-panel2 */
--fs-accent-base: #1564c0;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #eeeeee;
--fs-track: #eee;
--fs-good-base: #007936; /* --success-color-low */
--fs-warn-base: #826706; /* #efbd0b darkened; 1.61:1 as text is not a rounding error */
--fs-danger-base: #d00000; /* --error-color-low */
@@ -408,14 +409,14 @@
}
:root[data-palette="bootstrap"][data-darkmode="true"] {
/* DARK */
--fs-bg-base: #222222;
--fs-bg-base: #222;
--fs-panel-base: #282828;
/* bootstrap's own dark --background-color-low (#2c2c2c) sits 0.016 from --fs-panel, under
* the 0.02 the ramp check wants, so an app asking high/-low for a gradation gets one colour
* twice; #303030 is 0.031. Its light pair is 0.039 apart, so the same check catches this in
* the theme this palette is named after. */
--fs-panel2-base: #303030;
--fs-border-base: #444444;
--fs-border-base: #444;
--fs-text: #bfbfbf;
/* Bootstrap's dark has no third ink, so the two quiet tiers are this palette's own and AA
* decides where the fainter one sits: the export tier's -low follows --fs-faint, and on a
@@ -428,16 +429,16 @@
--fs-faint: #9a9a9a;
--fs-accent-base: #53a4c2;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
--fs-track: #333333;
--fs-track: #333;
--fs-good-base: #1aaf7b;
--fs-warn-base: #aa9968;
--fs-danger-base: #dd807e;
/* dark ink: these fills are light, and bootstrap's own --on-*-color in dark is likewise its
* --background-color-high. #222222 gives 5.65/5.65/5.66/5.65. */
--fs-on-accent: #222222;
--fs-on-good: #222222;
--fs-on-warn: #222222;
--fs-on-danger: #222222;
* --background-color-high. #222 gives 5.65/5.65/5.66/5.65. */
--fs-on-accent: #222;
--fs-on-good: #222;
--fs-on-warn: #222;
--fs-on-danger: #222;
}
/* ---- 2020 the OpenWrt 2020 theme's colourway ----
*
@@ -457,8 +458,8 @@
* reads --fs-panel, so in dark mode the navy is the CANVAS and the chrome sits on it. */
:root[data-palette="2020"] {
/* LIGHT — white content, the CI cyan darkened until it can be read on it */
--fs-bg-base: #ffffff;
--fs-panel-base: #ffffff;
--fs-bg-base: #fff;
--fs-panel-base: #fff;
--fs-panel2-base: #eef4f8;
--fs-border-base: #c3d2dd;
/* navy-tinted neutrals rather than grey: the ink belongs to the same scheme as the accent */
@@ -467,11 +468,10 @@
--fs-faint: #3d5566;
/* #00B5E2 held at hue, taken down until it reads: 5.89:1 on white against its own 2.09:1 */
--fs-accent-base: #036c92;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #e3ebf1;
--fs-good-base: #276e2a; /* #5CB85C darkened; 2.28:1 as text is not a rounding error */
--fs-warn-base: #8a5a00; /* #CC8800, same move */
--fs-danger-base: #c1121f; /* #CC1111 is 5.25:1 already — the one that stays where it was */
--fs-warn-base: #8a5a00; /* #C80, same move */
--fs-danger-base: #c1121f; /* #C11 is 5.25:1 already — the one that stays where it was */
/* ink ON the fills above — white clears AA on all four */
--fs-on-accent: #fff;
--fs-on-good: #fff;
@@ -503,8 +503,8 @@
/* ---- forum the OpenWrt forum's Discourse colourway (opt-in) ----
*
* Sampled from forum.openwrt.org's own `getComputedStyle(document.documentElement)`, one read
* per colour scheme: LIGHT --primary #222 (ink) / --secondary #fff (page) / --tertiary #0088cc
* (accent) / --quaternary and --danger both #e45735 / --success #009900 / --highlight #ffff4d;
* per colour scheme: LIGHT --primary #222 (ink) / --secondary #fff (page) / --tertiary #08c
* (accent) / --quaternary and --danger both #e45735 / --success #090 / --highlight #ffff4d;
* DARK --primary #ddd / --secondary #222 / --tertiary #0f82af / --danger #e45735 (unchanged
* from light) / --success #1ca551 / --highlight #a87137. Discourse names quaternary and
* danger as one colour in light and two in dark (#c14924); footstrap has no fifth semantic
@@ -526,20 +526,19 @@
* move, still the one furthest from AA after it. */
:root[data-palette="forum"] {
/* LIGHT — the forum's own page, exactly: white canvas, near-black ink */
--fs-bg-base: #ffffff;
--fs-panel-base: #ffffff;
--fs-bg-base: #fff;
--fs-panel-base: #fff;
--fs-panel2-base: #f0f0f0;
--fs-border-base: #dddddd;
--fs-text: #222222;
--fs-border-base: #ddd;
--fs-text: #222;
--fs-dim: #3d3d3d;
--fs-faint: #4a4a4a;
/* --tertiary #0088cc measures 3.89:1 on white; #0074ad held the hue at 5.12:1 there but
/* --tertiary #08c measures 3.89:1 on white; #0074ad held the hue at 5.12:1 there but
* only 4.49:1 on --fs-panel2, under the 4.5:1 floor the export tier holds every surface to;
* #0073ac holds it at 5.19:1 on white and 4.55:1 on --fs-panel2 */
--fs-accent-base: #0073ac;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #eeeeee;
/* --success #009900 measured 3.78:1, #008000 5.14:1; --quaternary/--danger #e45735
--fs-track: #eee;
/* --success #090 measured 3.78:1, #008000 5.14:1; --quaternary/--danger #e45735
* measured 3.68:1, #c63c1b 5.17:1; --highlight #ffff4d measured 1.07:1 as text, #717100
* 5.17:1 see the block comment above for why highlight fills warn */
--fs-good-base: #008000;
@@ -553,11 +552,11 @@
}
:root[data-palette="forum"][data-darkmode="true"] {
/* DARK — the forum's own dark scheme: #222 canvas, #ddd ink, both exactly as sampled */
--fs-bg-base: #222222;
--fs-bg-base: #222;
--fs-panel-base: #262626;
--fs-panel2-base: #2e2e2e;
--fs-border-base: #3a3a3a;
--fs-text: #dddddd;
--fs-text: #ddd;
--fs-dim: #a3a3a3;
--fs-faint: #969696;
/* --tertiary #0f82af measured 3.66:1 on --fs-bg and 3.12:1 on --fs-panel2, both under the
@@ -565,26 +564,24 @@
* 5.86:1 on --fs-bg and 5.00:1 on --fs-panel2 */
--fs-accent-base: #13a8e3;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
--fs-track: #333333;
--fs-track: #333;
/* --success #1ca551 measured 4.24:1 on --fs-panel2, #1fb65a 5.11:1; --danger #e45735
* measured 3.69:1, #eb8066 5.05:1; --highlight #a87137 measured 3.29:1, #c99259 5.00:1
* same reasoning as the light block for why highlight fills warn */
--fs-good-base: #1fb65a;
--fs-warn-base: #c99259;
--fs-danger-base: #eb8066;
/* dark ink: these fills are light, and #222222 (= --fs-bg-base) gives 5.86/5.98/5.86/5.92 */
--fs-on-accent: #222222;
--fs-on-good: #222222;
--fs-on-warn: #222222;
--fs-on-danger: #222222;
/* dark ink: these fills are light, and #222 (= --fs-bg-base) gives 5.86/5.98/5.86/5.92 */
--fs-on-accent: #222;
--fs-on-good: #222;
--fs-on-warn: #222;
--fs-on-danger: #222;
/* The masthead, and the one place the forum's navy belongs. rgb(0,43,73) is what
forum.openwrt.org paints its header with in its DARK scheme measured live; its light
scheme has a white header (--header_background #ffffff, ink #333333), which is what this
scheme has a white header (--header_background #fff, ink #333), which is what this
palette's light half already gives, so the navy is declared here and only here.
color-mix, not a flat fill: --fs-bar-bg is 88% of the panel over the page on purpose (a
bar is a thin strip the page ghosts through, 02-tokens.css), and a flat colour would drop
that. Over this palette's #222 canvas the strip lands near #06294a, where the bar's own
ink (#dddddd) measures 9.9:1 and its dimmed ink (#a3a3a3) 5.4:1. */
--fs-bar-bg: color-mix(in srgb, #002b49 88%, transparent);
A flat fill, like --fs-bar-bg everywhere (02-tokens.css): the bar's own ink (#ddd)
measures 10.7:1 on it and its dimmed ink (#a3a3a3) 5.8:1. */
--fs-bar-bg: #002b49;
}
}
@@ -47,17 +47,8 @@
--fs-focus-ring: var(--fs-focus-ring-solo);
--fs-focus-ring-invalid: 0 0 0 2px var(--fs-panel), 0 0 0 5px var(--fs-danger);
/* surfaces that are a near-opaque panel anyway */
--fs-glass: var(--fs-panel);
--fs-bar-bg: var(--fs-panel);
--background-color-medium: var(--fs-panel2);
/* depth cues: dropped rather than approximated each is a 1px highlight whose whole
* purpose is subtlety, and an opaque one reads as a stray line. */
--fs-emboss: none;
--fs-text-emboss: none;
--fs-shadow-bar: 0 1px 0 var(--fs-border);
/* the scrim over an uploaded photo: --fs-photo-dim is a percentage this cannot apply, so
* take the fixed dialog scrim. Losing it entirely would leave login text over a photo. */
--fs-photo-scrim: var(--fs-scrim);
@@ -177,7 +177,6 @@
white-space: nowrap;
/* surface: theme/45-misc.css (flat) — absorbed */
border-radius: var(--fs-radius-sm);
box-shadow: var(--fs-emboss);
display: inline-flex;
flex-direction: column;
/* a plate of single-line labels, not prose */
@@ -194,10 +193,6 @@
/* surface + ink: theme/45-misc.css — absorbed */
}
.ifacebox .ifacebox-head[style] {
text-shadow: var(--fs-text-emboss);
}
/* The !important fights luci-mod-network's inline zone-colour `style=`: a raw zone colour is too
* bright on a dark panel, so it is re-mixed at .4/.3.
*
@@ -231,9 +226,7 @@
padding: var(--fs-space-0-5);
/* surface: theme/45-misc.css (flat) — absorbed */
color: var(--fs-text);
text-shadow: var(--fs-text-emboss);
border-radius: var(--fs-radius-sm);
box-shadow: var(--fs-emboss);
cursor: default;
/* a badge is one line of label — the tight step, like .ifacebox above */
line-height: var(--fs-leading-tight);
@@ -328,7 +321,6 @@
display: inline-block;
white-space: nowrap;
color: var(--fs-text);
text-shadow: var(--fs-text-emboss);
}
.zonebadge > em,
@@ -11,7 +11,6 @@
background: var(--fs-panel);
border: var(--fs-hairline);
border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow);
padding: var(--fs-space-4);
}
@@ -59,18 +59,6 @@
background-size: var(--fs-pattern-size) auto;
}
/* A CBI form has dozens of .btn/.cbi-button; a backdrop-filter on each is a live
* backdrop-sampling layer over the pattern costly on weak GPUs, and invisible anyway
* since every button is already opaque. Blur dropped from buttons entirely; closed
* dropdowns keep the frost (few of them, and their body can show the pattern through). */
/* var(--fs-blur), not a literal: one radius for every frosted surface (02-tokens.css), and
* the literal 6px was also invisible to prefers-reduced-transparency, which nulls the token
* (95-a11y-media.css) the one frost that kept blurring for users who asked for opaque. */
:root[data-wallpaper="pattern"] .cbi-dropdown:not([open]) {
backdrop-filter: var(--fs-blur);
-webkit-backdrop-filter: var(--fs-blur);
}
/* No wash layer behind #view: the pattern shows through the gaps between the cards at
* full strength. Every block that carries text paints an opaque --fs-panel of its own,
* so nothing has to be dimmed globally. */
@@ -104,8 +104,6 @@
flex-direction: row; flex-wrap: wrap; align-items: center;
gap: var(--fs-space-2-5); padding: var(--fs-space-2-5) var(--fs-space-4);
background: var(--fs-bar-bg);
backdrop-filter: var(--fs-blur);
-webkit-backdrop-filter: var(--fs-blur);
border-inline-end: 0; border-bottom: var(--fs-hairline);
/* the submenu popups anchor to this bar and must escape it */
overflow: visible;
@@ -159,8 +157,7 @@
position: absolute; inset-inline: 0 auto; top: calc(100% + 8px); z-index: var(--fs-z-flyout);
width: max-content; min-width: 200px; max-width: calc(100vw - 32px);
margin: 0; padding: var(--fs-space-2);
background: var(--fs-glass);
backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur);
background: var(--fs-panel);
border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop);
}
@@ -323,7 +320,6 @@
flex-direction: column; flex-wrap: nowrap; align-items: normal;
gap: normal; padding: var(--fs-space-5) var(--fs-space-3-5);
background: var(--fs-panel);
backdrop-filter: none; -webkit-backdrop-filter: none;
border-inline-end: var(--fs-hairline); border-bottom: 0;
box-shadow: none;
/* `overflow: auto`, not overflow-y alone: the bar sets `overflow: visible` for its
@@ -388,7 +384,7 @@
width: auto; min-width: 0; max-width: none;
flex-direction: column; gap: var(--fs-space-0-5);
margin: 0 0 0 var(--fs-space-7); padding: 0;
background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
background: none;
border: 0; border-radius: 0; box-shadow: none;
overflow: hidden;
}
@@ -4,7 +4,6 @@
background: var(--fs-panel);
border: var(--fs-hairline);
border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow);
}
.cbi-section { padding: var(--fs-card-pad); margin-bottom: var(--fs-card-gap); }
/* a bare <legend>; the ones inside a .cbi-section are covered below. The ink is the heading
@@ -22,7 +22,6 @@
background-color: var(--fs-panel);
background-image: linear-gradient(var(--fs-alert-tint, transparent), var(--fs-alert-tint, transparent));
color: var(--fs-text);
box-shadow: var(--fs-shadow);
gap: var(--fs-space-3);
}
/* ---- status lives in the stripe and the surface; the ring is always neutral ----
@@ -23,7 +23,6 @@
background: var(--fs-panel);
border: var(--fs-hairline);
border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow);
/* kills a folder-tab baseline gradient (the 1px x 29px background image the strip
* conventionally carries) by zeroing its size rather than restating it base's copy is
* absorbed, but any gradient a view or third-party app sets is sized away too */
@@ -153,7 +153,6 @@
background: var(--fs-panel);
border: var(--fs-hairline);
border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow);
margin: 0 0 var(--fs-card-gap);
padding: var(--fs-space-4);
font-size: var(--fs-type-xl);
@@ -176,7 +175,6 @@
border: var(--fs-hairline);
border-top: 0;
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
box-shadow: var(--fs-shadow);
margin: 0 0 var(--fs-card-gap);
padding: 0 var(--fs-space-4) var(--fs-space-4);
color: var(--fs-dim);
@@ -28,7 +28,6 @@
/* align the bar's contents to the .fs-content column, so the brand and the menu line
* up with the headings and the cards below */
padding: 0 max(20px, calc((100% - var(--fs-content-max)) / 2 + var(--fs-content-pad)));
box-shadow: var(--fs-shadow-bar);
}
/* the menu rides the brand's row; its flex:1 opens the gap that pushes the right-hand
@@ -387,8 +387,10 @@
* lives in 03-palettes.css per palette/mode block, because a URI cannot read var(); it
* replaced a hardcoded grey that ignored palette and dark mode. */
select, select.cbi-input-select, .cbi-value-field select {
/* -webkit- only: Firefox has had the unprefixed property since 80. */
appearance: none; -webkit-appearance: none;
/* unprefixed only: the floor (Chrome 108/Firefox 101/Safari 15.4) clears unprefixed
* `appearance` everywhere (Chrome 84, Firefox 80, Safari 15.4 docs/css.md). -webkit- was
* 96 B across these four rules for nothing this sheet still has to run on. */
appearance: none;
background-color: var(--fs-panel2);
background-image: var(--fs-select-chevron);
/* right inset from the scale, not a literal 12 the .cbi-select shell below states the same
@@ -506,7 +508,7 @@
* one inside the 32px shell. */
.cbi-select select,
.cbi-select select:focus {
appearance: none; -webkit-appearance: none;
appearance: none;
outline: none;
border: none;
background: transparent;
@@ -782,22 +784,20 @@
/* RangeSlider (UIRangeSlider) the one LuCI widget base leaves to native browser rendering.
* Token-driven track/thumb. */
input[type="range"] {
-webkit-appearance: none; appearance: none;
appearance: none;
width: 100%; height: 6px; min-height: 0; padding: 0;
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
outline: none; box-shadow: none; cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
appearance: none;
width: 16px; height: 16px; border-radius: 50%;
background: var(--fs-accent); border: 2px solid var(--fs-panel);
box-shadow: var(--fs-shadow); cursor: pointer;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 16px; height: 16px; border-radius: 50%;
background: var(--fs-accent); border: 2px solid var(--fs-panel); cursor: pointer;
/* same shadow the -webkit thumb carries — Firefox had silently drifted flat */
box-shadow: var(--fs-shadow);
}
input[type="range"]::-moz-range-track { background: var(--fs-track); height: 6px; border-radius: var(--fs-radius-pill); }
/* -solo: the track sets `border: 0` and `outline: none`, so there is no second channel here
@@ -26,8 +26,7 @@
.fs-search-box {
display: flex; flex-direction: column; min-height: 0;
width: 100%; max-width: 560px; max-height: min(70dvh, 560px);
background: var(--fs-glass);
backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur);
background: var(--fs-panel);
border: var(--fs-hairline); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow-pop);
overflow: hidden;
@@ -21,16 +21,11 @@
}
}
/* prefers-reduced-transparency the user asked the OS for opaque surfaces. Every frosted surface
* in this theme is a translucent fill plus a backdrop-filter blur, and both go: the fill becomes
* the opaque panel, and the blur the most expensive paint on a phone GPU, the sticky bar's
* backdrop being resampled on every scrolled frame has nothing left to show through. This also
* gives a measured escape hatch on devices where that blur is too costly. */
/* prefers-reduced-transparency the user asked the OS for opaque surfaces. Every surface in the
* theme is opaque already: no blur, an opaque bar, opaque pop surfaces. The one thing left is the
* wash over an uploaded photo. */
@media (prefers-reduced-transparency: reduce) {
:root {
--fs-bar-bg: var(--fs-panel);
--fs-glass: var(--fs-panel);
--fs-blur: none;
/* the uploaded background is a translucent wash over a photo exactly the effect this
* user asked the OS to drop. Make the scrim opaque so the canvas is flat, not a photo
* ghosting through (16-login-bg.css lays it over the image as one background layer). */
@@ -17,9 +17,8 @@
--fs-bg: #fff; --fs-panel: #fff; --fs-panel2: #f4f4f4;
--fs-text: #000; --fs-dim: #333; --fs-faint: #555;
--fs-border: #bbb; --fs-track: #ddd;
--fs-shadow: none; --fs-shadow-pop: none; --fs-shadow-bar: none;
--fs-emboss: none; --fs-text-emboss: none;
--fs-bar-bg: #fff; --fs-glass: #fff; --fs-blur: none;
--fs-shadow-pop: none;
--fs-bar-bg: #fff;
}
/* Navigation, the search overlay and the action buttons cannot be used on paper. The
* Appearance popover was on this list; it is a PAGE now, and a page prints an admin