diff --git a/dae/Makefile b/dae/Makefile
index 7c4528be..16e83f74 100644
--- a/dae/Makefile
+++ b/dae/Makefile
@@ -10,7 +10,7 @@ PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_PROTO:=git
-PKG_SOURCE_VERSION:=9bb42373173a609e5aa253ffceae81b982fbf36a
+PKG_SOURCE_VERSION:=64dd36796f74660421d30bfa7b935797391ca428
PKG_SOURCE_URL:=https://github.com/olicesx/dae.git
PKG_MIRROR_HASH:=skip
diff --git a/luci-app-amlogic/Makefile b/luci-app-amlogic/Makefile
index 571d1336..872090ba 100644
--- a/luci-app-amlogic/Makefile
+++ b/luci-app-amlogic/Makefile
@@ -16,7 +16,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-amlogic
-PKG_VERSION:=3.1.319
+PKG_VERSION:=3.1.320
PKG_RELEASE:=2
PKG_LICENSE:=GPL-2.0 License
diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js
index cec86c7c..b05ffd3b 100644
--- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js
+++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js
@@ -57,22 +57,24 @@ return view.extend({
handleSaveApply: null,
handleReset: null,
- // On load, call sync_menu to update sidebar menu flags from runtime platform detection. If the install menu becomes
- // visible but is not in the current nav, reload once to pick up the new menu (guarded by #menu-synced hash).
- load: function () {
- // Call sync_menu; if the install menu is now visible on the server but
- // absent in the browser nav, reload once (guarded by #menu-synced hash).
+ // On load, call sync_menu to update sidebar menu flags from runtime platform detection. If the sidebar nav is out of
+ // sync with the server-side menu flags, reload once to pick up the new menu (guarded by #menu-synced hash).
+ load: function () {
+ // Call sync_menu; reload once when the sidebar nav disagrees with the
+ // server-side menu flags in either direction (guarded by #menu-synced hash).
const alreadyReloaded = window.location.hash === '#menu-synced';
const syncMenuPromise = callSyncMenu().then(function (res) {
- if (alreadyReloaded) return;
- if (res && res.show_install === 'yes') {
- // Check if the sidebar navigation already contains the install link.
- const installLink = document.querySelector('a[href*="amlogic/install"]');
- if (!installLink) {
- // Server-side index cache updated; reload to pick up new nav.
- window.location.replace(window.location.pathname + '#menu-synced');
- window.location.reload();
- }
+ if (alreadyReloaded || !res) return;
+ // Check if the sidebar navigation already contains the install link.
+ const installLink = document.querySelector('a[href*="amlogic/install"]');
+ // Menu became visible on the server but is absent in the browser nav.
+ const needShow = (res.show_install === 'yes' && !installLink);
+ // Menu became hidden on the server but is still present in the browser nav.
+ const needHide = (res.show_install === 'no' && installLink);
+ if (needShow || needHide) {
+ // Server-side index cache updated; reload to pick up new nav.
+ window.location.replace(window.location.pathname + '#menu-synced');
+ window.location.reload();
}
}).catch(function () {});
return Promise.all([callAuthor(), syncMenuPromise]).then(function (r) { return r[0]; });
diff --git a/luci-app-amlogic/root/etc/uci-defaults/luci-amlogic b/luci-app-amlogic/root/etc/uci-defaults/luci-amlogic
index bbc84bf8..231c0c79 100755
--- a/luci-app-amlogic/root/etc/uci-defaults/luci-amlogic
+++ b/luci-app-amlogic/root/etc/uci-defaults/luci-amlogic
@@ -12,10 +12,28 @@ PLATFORM=$(grep -E '^PLATFORM=' /etc/flippy-openwrt-release 2>/dev/null | \
SHOW_INSTALL=$(grep -E '^SHOW_INSTALL_MENU=' /etc/flippy-openwrt-release 2>/dev/null | \
awk -F= '{print $2}' | tr -d '"' | xargs)
-case "$PLATFORM" in
- *amlogic*) MENU_INSTALL=yes ;;
- *) [ "$SHOW_INSTALL" = "yes" ] && MENU_INSTALL=yes || MENU_INSTALL=no ;;
+# Detect whether the root fs is already on eMMC (mmcblk*p* with device/type == "MMC").
+# Mirrors root_on_internal_storage() in luci.amlogic: all other boot media
+# (TF/SD, NVMe, SATA, USB) keep the install menu visible.
+ROOT_PT=$(df / | tail -n1 | awk '{print $1}' | awk -F '/' '{print $3}')
+IS_INSTALLED=0
+case "$ROOT_PT" in
+ mmcblk*p*)
+ BASE_DEV="${ROOT_PT%p*}"
+ [ "$(cat /sys/block/${BASE_DEV}/device/type 2>/dev/null | xargs)" = "MMC" ] && IS_INSTALLED=1
+ ;;
esac
+
+case "$PLATFORM" in
+ *amlogic*|*allwinner*) CAN_INSTALL=yes ;;
+ *) [ "$SHOW_INSTALL" = "yes" ] && CAN_INSTALL=yes || CAN_INSTALL=no ;;
+esac
+if [ "$CAN_INSTALL" = "yes" ] && [ "$IS_INSTALLED" = "0" ]; then
+ MENU_INSTALL=yes
+else
+ MENU_INSTALL=no
+fi
+
case "$PLATFORM" in
*qemu*) MENU_ARMCPU=no ;;
*) MENU_ARMCPU=yes ;;
diff --git a/luci-app-kaiplus/luasrc/controller/kaiplus.lua b/luci-app-kaiplus/luasrc/controller/kaiplus.lua
index 769bd805..9a0c080b 100644
--- a/luci-app-kaiplus/luasrc/controller/kaiplus.lua
+++ b/luci-app-kaiplus/luasrc/controller/kaiplus.lua
@@ -15,10 +15,12 @@ end
function kaiplus_status()
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
- local port = uci:get_first("kaiplus", "kaiplus", "port", "8198")
+ local port = uci:get_first("kaiplus", "kaiplus", "port", "8189")
+ local base_path = uci:get_first("kaiplus", "kaiplus", "base_path", "/apps/kaiplus/")
local status = {
running = (sys.call("pidof kaiplus_bin >/dev/null") == 0),
- port = port
+ port = port,
+ base_path = base_path
}
luci.http.prepare_content("application/json")
luci.http.write_json(status)
diff --git a/luci-app-kaiplus/luasrc/model/cbi/kaiplus.lua b/luci-app-kaiplus/luasrc/model/cbi/kaiplus.lua
index 91de5648..15a3a29d 100644
--- a/luci-app-kaiplus/luasrc/model/cbi/kaiplus.lua
+++ b/luci-app-kaiplus/luasrc/model/cbi/kaiplus.lua
@@ -5,7 +5,10 @@ m = Map("kaiplus", translate("KaiPlus"), translate("KaiPlus is an AI workspace a
m:section(SimpleSection).template = "kaiplus/kaiplus_status"
m.on_after_commit = function(self)
- sys.call("/etc/init.d/kaiplus restart >/dev/null 2>&1 &")
+ sys.call("/etc/init.d/kaiplus restart >/dev/null 2>&1")
+ if sys.call("[ -x /etc/init.d/linkease ]") == 0 then
+ sys.call("/etc/init.d/linkease restart >/dev/null 2>&1 &")
+ end
end
s = m:section(TypedSection, "kaiplus", translate("Global settings"))
@@ -29,7 +32,7 @@ end
data_dir.default = default_path
local port = s:option(Value, "port", translate("Web port"))
-port.default = "8198"
+port.default = "8189"
port.rmempty = false
port.datatype = "port"
port.description = translate("Port for the KaiPlus web service.")
diff --git a/luci-app-kaiplus/luasrc/view/kaiplus/kaiplus_status.htm b/luci-app-kaiplus/luasrc/view/kaiplus/kaiplus_status.htm
index 147688eb..7fb4da6f 100644
--- a/luci-app-kaiplus/luasrc/view/kaiplus/kaiplus_status.htm
+++ b/luci-app-kaiplus/luasrc/view/kaiplus/kaiplus_status.htm
@@ -6,8 +6,15 @@ XHR.poll(5, '<%=url("admin/services/kaiplus_status")%>', null, function(x, st) {
el.innerHTML = '
<%:The KaiPlus service is not running.%>';
}
if (st.running) {
+ var basePath = st.base_path || "/apps/kaiplus/";
+ if (basePath.charAt(0) !== "/") {
+ basePath = "/" + basePath;
+ }
+ if (basePath.charAt(basePath.length - 1) !== "/") {
+ basePath = basePath + "/";
+ }
el.innerHTML = '
<%:The KaiPlus service is running.%>'
- + "
\" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
+ + "
\" onclick=\"window.open('http://' + window.location.hostname + ':' + st.port + basePath)\"/>";
}
}
});
diff --git a/luci-app-linkeasefull/Makefile b/luci-app-linkeasefull/Makefile
new file mode 100644
index 00000000..1219d4b0
--- /dev/null
+++ b/luci-app-linkeasefull/Makefile
@@ -0,0 +1,18 @@
+#
+# Copyright (C) 2026 iStoreOS
+#
+# This is free software, licensed under the Apache License, Version 2.0 .
+#
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=LuCI support for linkeasefull
+LUCI_DEPENDS:=+linkeasefull
+LUCI_PKGARCH:=all
+
+PKG_VERSION:=1.0.0
+PKG_RELEASE:=1
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-app-linkeasefull/luasrc/controller/linkeasefull.lua b/luci-app-linkeasefull/luasrc/controller/linkeasefull.lua
new file mode 100644
index 00000000..bed1f88f
--- /dev/null
+++ b/luci-app-linkeasefull/luasrc/controller/linkeasefull.lua
@@ -0,0 +1,89 @@
+module("luci.controller.linkeasefull", package.seeall)
+
+function index()
+ if not nixio.fs.access("/etc/config/linkeasefull") then
+ return
+ end
+
+ entry({"admin", "services", "linkeasefull"}, cbi("linkeasefull"), _("LinkEase Full"), 21).dependent = true
+ entry({"admin", "services", "linkeasefull_status"}, call("linkeasefull_status"))
+ entry({"admin", "services", "linkeasefull", "auth"}, call("linkeasefull_auth")).leaf = true
+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=http://127.0.0.1:19290" then
+ return true
+ end
+ end
+ return false
+end
+
+function linkeasefull_status()
+ local sys = require "luci.sys"
+ local uci = require "luci.model.uci".cursor()
+
+ local status = {
+ running = (sys.call("pidof linkease-full >/dev/null") == 0),
+ full_port = 19290,
+ legacy_port = 8897,
+ base_path = "/apps/",
+ lan_ip = uci:get("network", "lan", "ipaddr") or "",
+ proxy_prefix_enabled = uhttpd_has_apps_proxy_prefix()
+ }
+
+ luci.http.prepare_content("application/json")
+ luci.http.write_json(status)
+end
+
+local function retrieve_luci_session()
+ local http = require "luci.http"
+ local util = require "luci.util"
+
+ for _, key in ipairs({"sysauth_https", "sysauth_http", "sysauth"}) do
+ local sid = http.getcookie(key)
+ if sid and sid ~= "" then
+ local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
+ if sdat and type(sdat.values) == "table" then
+ return sid
+ end
+ end
+ end
+ return nil
+end
+
+local function valid_apps_return(value)
+ if not value or value == "" then
+ return false
+ end
+ if value == "/apps" then
+ return true
+ end
+ local prefix = value:sub(1, 6)
+ return prefix == "/apps/" or prefix == "/apps?" or prefix == "/apps#"
+end
+
+local function valid_cookie_value(value)
+ return value and value:match("^[A-Za-z0-9._%-_]+$") ~= nil
+end
+
+function linkeasefull_auth()
+ local http = require "luci.http"
+ local sid = retrieve_luci_session()
+
+ if not valid_cookie_value(sid) then
+ http.status(403, "Forbidden")
+ return
+ end
+
+ local target = http.formvalue("return") or "/apps/"
+ if not valid_apps_return(target) then
+ target = "/apps/"
+ end
+
+ http.header("Set-Cookie", "linkease_openwrt_sid=" .. sid .. "; Path=/apps; HttpOnly; SameSite=Lax")
+ http.redirect(target)
+end
diff --git a/luci-app-linkeasefull/luasrc/model/cbi/linkeasefull.lua b/luci-app-linkeasefull/luasrc/model/cbi/linkeasefull.lua
new file mode 100644
index 00000000..23950bc4
--- /dev/null
+++ b/luci-app-linkeasefull/luasrc/model/cbi/linkeasefull.lua
@@ -0,0 +1,86 @@
+local fs = require "nixio.fs"
+
+local m, s
+
+local function starts_with(value, prefix)
+ return value:sub(1, #prefix) == prefix
+end
+
+local function decode_mount_path(value)
+ return value:gsub("\\040", " "):gsub("\\011", "\t"):gsub("\\012", "\n"):gsub("\\134", "\\")
+end
+
+local function is_persistent_mount(path, fstype)
+ if path == "" or path == "/" then
+ return false
+ end
+ if starts_with(path, "/tmp") or starts_with(path, "/var") or starts_with(path, "/run") then
+ return false
+ end
+ if starts_with(path, "/dev") or starts_with(path, "/proc") or starts_with(path, "/sys") then
+ return false
+ end
+ if starts_with(path, "/rom") or starts_with(path, "/overlay") then
+ return false
+ end
+ if fstype == "tmpfs" or fstype == "devtmpfs" or fstype == "overlay" or fstype == "squashfs" then
+ return false
+ end
+ return true
+end
+
+local function storage_mounts()
+ local mounts = {}
+ for line in io.lines("/proc/mounts") do
+ local device, path, fstype = line:match("^(%S+)%s+(%S+)%s+(%S+)")
+ path = path and decode_mount_path(path) or ""
+ if device and fstype and is_persistent_mount(path, fstype) then
+ mounts[#mounts + 1] = {
+ path = path,
+ label = string.format("%s (%s)", path, fstype)
+ }
+ end
+ end
+ table.sort(mounts, function(a, b) return a.path < b.path end)
+ return mounts
+end
+
+m = Map("linkeasefull", translate("LinkEase Full"), translate("LinkEase Full uses fixed entries at port 19290 /apps/ and legacy port 8897."))
+
+m:section(SimpleSection).template = "linkeasefull_status"
+
+s = m:section(TypedSection, "linkeasefull", translate("Storage"))
+s.addremove = false
+s.anonymous = true
+
+s:option(Flag, "enabled", translate("Enable")).rmempty = false
+
+local data = s:option(ListValue, "data_root_parent", translate("Storage path"), translate("Choose a mounted persistent disk. /tmp is not allowed."))
+data.rmempty = false
+
+local current = m.uci:get_first("linkeasefull", "linkeasefull", "data_root_parent") or ""
+local has_current = false
+for _, mount in ipairs(storage_mounts()) do
+ data:value(mount.path, mount.label)
+ if mount.path == current then
+ has_current = true
+ end
+end
+if current ~= "" and not has_current and fs.stat(current, "type") == "dir" and is_persistent_mount(current, "") then
+ data:value(current, current)
+end
+
+function data.validate(self, value)
+ if value == nil or value == "" then
+ return nil, translate("Please choose a mounted persistent disk.")
+ end
+ if not is_persistent_mount(value, "") then
+ return nil, translate("/tmp and system paths cannot be used for LinkEase Full storage.")
+ end
+ if fs.stat(value, "type") ~= "dir" then
+ return nil, translate("The selected storage path does not exist.")
+ end
+ return value
+end
+
+return m
diff --git a/luci-app-linkeasefull/luasrc/view/linkeasefull_status.htm b/luci-app-linkeasefull/luasrc/view/linkeasefull_status.htm
new file mode 100644
index 00000000..9637232e
--- /dev/null
+++ b/luci-app-linkeasefull/luasrc/view/linkeasefull_status.htm
@@ -0,0 +1,34 @@
+
+
+