Repository: koshev-msk/modemfeed Branch: master Commit: c535e0c1c796 Files: 387 Total size: 1.2 MB Directory structure: gitextract_vn7k3biq/ ├── .gitmodules ├── README.md ├── luci/ │ ├── applications/ │ │ ├── luci-app-3proxy/ │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── proxy/ │ │ │ │ └── 3proxy.js │ │ │ ├── po/ │ │ │ │ └── ru/ │ │ │ │ └── 3proxy.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-3proxy.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-3proxy.json │ │ ├── luci-app-atinout/ │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── modem/ │ │ │ │ ├── atcommands.js │ │ │ │ └── atconfig.js │ │ │ ├── po/ │ │ │ │ ├── ru/ │ │ │ │ │ └── atinout.po │ │ │ │ ├── template/ │ │ │ │ │ └── atinout.pot │ │ │ │ └── zh_Hans/ │ │ │ │ └── atinout.po │ │ │ └── root/ │ │ │ ├── etc/ │ │ │ │ └── uci-defaults/ │ │ │ │ └── 65-luci-app-atinout │ │ │ └── usr/ │ │ │ ├── bin/ │ │ │ │ └── luci-app-atinout │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-atinout.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-atinout.json │ │ ├── luci-app-cellled/ │ │ │ ├── Makefile │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── cellled.js │ │ │ ├── po/ │ │ │ │ └── ru/ │ │ │ │ └── cellled.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-cellled.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-cellled.json │ │ ├── luci-app-mmconfig/ │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── modem/ │ │ │ │ └── bands.js │ │ │ ├── po/ │ │ │ │ ├── ru/ │ │ │ │ │ └── mmconfig.po │ │ │ │ └── zh_Hans/ │ │ │ │ └── mmconfig.po │ │ │ └── root/ │ │ │ ├── etc/ │ │ │ │ ├── config/ │ │ │ │ │ └── mmconfig │ │ │ │ ├── init.d/ │ │ │ │ │ └── mmconfig │ │ │ │ └── uci-defaults/ │ │ │ │ └── 70_luci-app-mmconfig │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-mmconfig.json │ │ │ ├── modeminfo/ │ │ │ │ └── mmconfig │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-mmconfig.json │ │ ├── luci-app-modeminfo/ │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ ├── modem-indicator.js │ │ │ │ ├── preload/ │ │ │ │ │ └── modem-indicator.js │ │ │ │ └── view/ │ │ │ │ ├── modem/ │ │ │ │ │ ├── hw.js │ │ │ │ │ ├── main.js │ │ │ │ │ └── setup.js │ │ │ │ └── status/ │ │ │ │ └── include/ │ │ │ │ └── 26_modeminfo.js │ │ │ ├── po/ │ │ │ │ ├── ru/ │ │ │ │ │ └── modeminfo.po │ │ │ │ └── zh_Hans/ │ │ │ │ └── modeminfo.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-modeminfo.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-modeminfo.json │ │ ├── luci-app-mwan3-ledhelper/ │ │ │ ├── Makefile │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── mwan3/ │ │ │ │ └── network/ │ │ │ │ └── led.js │ │ │ ├── po/ │ │ │ │ └── ru/ │ │ │ │ └── mwan3-ledhelper.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-mwan3-ledhelper.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-mwan3-ledhelper.json │ │ ├── luci-app-ota/ │ │ │ ├── Makefile │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── system/ │ │ │ │ └── ota.js │ │ │ ├── po/ │ │ │ │ └── ru/ │ │ │ │ └── ota.po │ │ │ └── root/ │ │ │ ├── etc/ │ │ │ │ └── config/ │ │ │ │ └── ota │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-ota.json │ │ │ ├── ota.sh │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-ota.json │ │ ├── luci-app-pingcontrol/ │ │ │ ├── Makefile │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── pingcontrol/ │ │ │ │ └── pingcontrol.js │ │ │ ├── po/ │ │ │ │ ├── en/ │ │ │ │ │ └── pingcontrol.po │ │ │ │ └── ru/ │ │ │ │ └── pingcontrol.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-pingcontrol.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-pingcontrol.json │ │ ├── luci-app-rtorrent/ │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── cgi-bin/ │ │ │ │ └── rtorrent │ │ │ ├── luasrc/ │ │ │ │ ├── controller/ │ │ │ │ │ └── rtorrent.lua │ │ │ │ ├── model/ │ │ │ │ │ └── cbi/ │ │ │ │ │ └── rtorrent/ │ │ │ │ │ ├── add.lua │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── rss.lua │ │ │ │ │ │ └── rtorrent.lua │ │ │ │ │ ├── common.lua │ │ │ │ │ ├── download.lua │ │ │ │ │ ├── main.lua │ │ │ │ │ ├── rss-rule.lua │ │ │ │ │ ├── rss.lua │ │ │ │ │ ├── string.lua │ │ │ │ │ └── torrent/ │ │ │ │ │ ├── files.lua │ │ │ │ │ ├── info.lua │ │ │ │ │ ├── peers.lua │ │ │ │ │ └── trackers.lua │ │ │ │ └── view/ │ │ │ │ └── rtorrent/ │ │ │ │ ├── button.htm │ │ │ │ ├── buttonsection.htm │ │ │ │ ├── empty.htm │ │ │ │ ├── fvalue.htm │ │ │ │ ├── list.htm │ │ │ │ ├── lvalue.htm │ │ │ │ ├── rss_addrule.htm │ │ │ │ ├── tabmenu.htm │ │ │ │ └── tvalue.htm │ │ │ ├── po/ │ │ │ │ ├── en/ │ │ │ │ │ └── rtorrent.po │ │ │ │ └── ru/ │ │ │ │ └── rtorrent.po │ │ │ └── root/ │ │ │ ├── etc/ │ │ │ │ ├── config/ │ │ │ │ │ └── rtorrent │ │ │ │ ├── cookies.txt │ │ │ │ ├── init.d/ │ │ │ │ │ └── rtorrent │ │ │ │ ├── rtorrent.conf │ │ │ │ └── uci-defaults/ │ │ │ │ └── rtorrent │ │ │ └── usr/ │ │ │ └── lib/ │ │ │ └── lua/ │ │ │ ├── bencode.lua │ │ │ ├── rss_downloader.lua │ │ │ ├── rtorrent.lua │ │ │ └── xmlrpc/ │ │ │ ├── init.lua │ │ │ └── scgi.lua │ │ ├── luci-app-smstools3/ │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── smstools3/ │ │ │ │ ├── cmd.js │ │ │ │ ├── in.js │ │ │ │ ├── out.js │ │ │ │ ├── pb.js │ │ │ │ ├── script.js │ │ │ │ ├── send.js │ │ │ │ └── setup.js │ │ │ ├── po/ │ │ │ │ ├── ru/ │ │ │ │ │ └── smstools3.po │ │ │ │ └── zh_Hans/ │ │ │ │ └── smstools3.po │ │ │ └── root/ │ │ │ ├── etc/ │ │ │ │ ├── init.d/ │ │ │ │ │ └── luci-sms │ │ │ │ └── uci-defaults/ │ │ │ │ └── 63_luci-app-smstools3 │ │ │ └── usr/ │ │ │ ├── bin/ │ │ │ │ ├── msg_control │ │ │ │ └── send_sms │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-smstools3.json │ │ │ ├── luci-app-smstools3/ │ │ │ │ ├── event.sh │ │ │ │ ├── led.sh │ │ │ │ ├── smscommand.sh │ │ │ │ └── smstools3.user │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-smstools3.json │ │ ├── luci-app-ssw/ │ │ │ ├── Makefile │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── modem/ │ │ │ │ └── ssw.js │ │ │ ├── po/ │ │ │ │ └── ru/ │ │ │ │ └── ssw.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-ssw.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-ssw.json │ │ ├── luci-app-telegrambot/ │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── view/ │ │ │ │ └── telegrambot.js │ │ │ ├── po/ │ │ │ │ └── ru/ │ │ │ │ └── telegrambot.po │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ ├── luci/ │ │ │ │ └── menu.d/ │ │ │ │ └── luci-app-telegrambot.json │ │ │ └── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-telegrambot.json │ │ └── luci-app-ttl/ │ │ ├── Makefile │ │ ├── htdocs/ │ │ │ └── luci-static/ │ │ │ └── resources/ │ │ │ └── view/ │ │ │ └── firewall/ │ │ │ └── ttl.js │ │ ├── po/ │ │ │ └── ru/ │ │ │ └── ttl.po │ │ └── root/ │ │ ├── etc/ │ │ │ ├── config/ │ │ │ │ └── ttl │ │ │ ├── hotplug.d/ │ │ │ │ └── iface/ │ │ │ │ └── 90-ttl │ │ │ └── init.d/ │ │ │ └── ttl │ │ └── usr/ │ │ └── share/ │ │ ├── luci/ │ │ │ └── menu.d/ │ │ │ └── luci-app-ttl.json │ │ ├── rpcd/ │ │ │ └── acl.d/ │ │ │ └── luci-app-ttl.json │ │ ├── ttl.sh │ │ ├── ttlipt.sh │ │ └── ttlnft.sh │ ├── protocols/ │ │ ├── luci-proto-openvpn/ │ │ │ ├── Makefile │ │ │ ├── htdocs/ │ │ │ │ └── luci-static/ │ │ │ │ └── resources/ │ │ │ │ └── protocol/ │ │ │ │ └── openvpn.js │ │ │ └── root/ │ │ │ └── usr/ │ │ │ └── share/ │ │ │ └── rpcd/ │ │ │ ├── acl.d/ │ │ │ │ └── luci-proto-openvpn.json │ │ │ └── ucode/ │ │ │ └── luci.openvpn.uc │ │ ├── luci-proto-tun2socks/ │ │ │ ├── Makefile │ │ │ └── htdocs/ │ │ │ └── luci-static/ │ │ │ └── resources/ │ │ │ └── protocol/ │ │ │ └── t2s.js │ │ └── luci-proto-xmm/ │ │ ├── Makefile │ │ ├── README.md │ │ └── htdocs/ │ │ └── luci-static/ │ │ └── resources/ │ │ └── protocol/ │ │ └── xmm.js │ └── themes/ │ ├── luci-theme-lightblue/ │ │ ├── Makefile │ │ ├── htdocs/ │ │ │ └── luci-static/ │ │ │ ├── lightblue/ │ │ │ │ ├── cascade.css │ │ │ │ └── mobile.css │ │ │ └── resources/ │ │ │ └── menu-lightblue.js │ │ ├── luasrc/ │ │ │ └── view/ │ │ │ └── themes/ │ │ │ └── lightblue/ │ │ │ ├── footer.htm │ │ │ └── header.htm │ │ └── root/ │ │ └── etc/ │ │ └── uci-defaults/ │ │ └── 30_luci-theme-lightblue │ ├── luci-theme-merona/ │ │ ├── Makefile │ │ ├── htdocs/ │ │ │ └── luci-static/ │ │ │ ├── merona/ │ │ │ │ ├── cascade.css │ │ │ │ ├── checkbox.css │ │ │ │ └── mobile.css │ │ │ └── resources/ │ │ │ └── menu-merona.js │ │ ├── luasrc/ │ │ │ └── view/ │ │ │ └── themes/ │ │ │ └── merona/ │ │ │ ├── footer.htm │ │ │ └── header.htm │ │ └── root/ │ │ └── etc/ │ │ └── uci-defaults/ │ │ └── 30_luci-theme-merona │ ├── luci-theme-routerich/ │ │ ├── Makefile │ │ ├── htdocs/ │ │ │ └── luci-static/ │ │ │ ├── resources/ │ │ │ │ └── menu-routerich.js │ │ │ └── routerich/ │ │ │ ├── background/ │ │ │ │ └── README.md │ │ │ ├── css/ │ │ │ │ ├── cascade.css │ │ │ │ ├── dark.css │ │ │ │ └── routerich.css │ │ │ └── icon/ │ │ │ ├── browserconfig.xml │ │ │ └── manifest.json │ │ ├── luasrc/ │ │ │ └── view/ │ │ │ └── themes/ │ │ │ └── routerich/ │ │ │ ├── footer.htm │ │ │ ├── footer_login.htm │ │ │ ├── header.htm │ │ │ ├── header_login.htm │ │ │ ├── out_header_login.htm │ │ │ └── sysauth.htm │ │ └── root/ │ │ └── etc/ │ │ └── uci-defaults/ │ │ └── 30_luci-theme-routerich │ └── luci-theme-teleofis/ │ ├── Makefile │ ├── htdocs/ │ │ └── luci-static/ │ │ ├── resources/ │ │ │ └── menu-teleofis.js │ │ └── teleofis/ │ │ ├── cascade.css │ │ ├── checkbox.css │ │ └── mobile.css │ ├── luasrc/ │ │ └── view/ │ │ └── themes/ │ │ └── teleofis/ │ │ ├── footer.htm │ │ └── header.htm │ └── root/ │ └── etc/ │ └── uci-defaults/ │ └── 30_luci-theme-teleofis └── packages/ ├── net/ │ ├── 3proxy/ │ │ ├── Makefile │ │ ├── README.md │ │ ├── files/ │ │ │ └── etc/ │ │ │ ├── 3proxy.cfg │ │ │ ├── config/ │ │ │ │ └── 3proxy │ │ │ └── init.d/ │ │ │ └── 3proxy │ │ └── patches/ │ │ ├── 0001-fix-makefile-loff_t-and-enable-ssl-plugin.patch │ │ └── 0002-use-pcre-library.patch │ ├── accel-ppp/ │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── files/ │ │ │ ├── accel-ppp.conf │ │ │ └── accel-ppp.init │ │ └── patches/ │ │ └── 0001-build-with-musl.patch │ ├── cellled/ │ │ ├── Makefile │ │ └── root/ │ │ ├── etc/ │ │ │ ├── config/ │ │ │ │ └── cellled │ │ │ └── init.d/ │ │ │ └── cellled │ │ └── usr/ │ │ ├── bin/ │ │ │ └── cellled │ │ └── share/ │ │ └── cellled.sh │ ├── ethstatus/ │ │ ├── Makefile │ │ └── patches/ │ │ └── 0001-version.patch │ ├── ifmetric/ │ │ ├── Makefile │ │ └── patches/ │ │ └── 0001-nlrequest.c_packet-too-small_fix │ ├── maxminddb-dump-country/ │ │ └── Makefile │ ├── modeminfo/ │ │ ├── Makefile │ │ └── root/ │ │ ├── etc/ │ │ │ └── config/ │ │ │ └── modeminfo │ │ └── usr/ │ │ ├── bin/ │ │ │ └── modeminfo │ │ ├── lib/ │ │ │ └── telegrambot/ │ │ │ └── plugins/ │ │ │ └── modeminfo.sh │ │ └── share/ │ │ ├── modeminfo/ │ │ │ ├── modem.list │ │ │ ├── modeminfo │ │ │ └── scripts/ │ │ │ ├── DELL │ │ │ ├── DELL.at │ │ │ ├── DIS_SIMCOM_A7XXX │ │ │ ├── DIS_SIMCOM_A7XXX.at │ │ │ ├── FIBOCOM │ │ │ ├── FIBOCOM.at │ │ │ ├── GENERIC │ │ │ ├── GENERIC.at │ │ │ ├── GOSUN │ │ │ ├── GOSUN.at │ │ │ ├── HUAWEI │ │ │ ├── HUAWEI.at │ │ │ ├── INTEL │ │ │ ├── INTEL.at │ │ │ ├── INTEL_FM350 │ │ │ ├── INTEL_FM350.at │ │ │ ├── MEIGLINK │ │ │ ├── MEIGLINK.at │ │ │ ├── MIKROTIK │ │ │ ├── MIKROTIK.at │ │ │ ├── QUALCOMM │ │ │ ├── QUALCOMM.at │ │ │ ├── QUECTEL │ │ │ ├── QUECTEL.at │ │ │ ├── SIERRA │ │ │ ├── SIERRA.at │ │ │ ├── SIMCOM │ │ │ ├── SIMCOM.at │ │ │ ├── SKELETON │ │ │ ├── SKELETON.at │ │ │ ├── STYX │ │ │ ├── STYX.at │ │ │ ├── THALES │ │ │ ├── THALES.at │ │ │ ├── THINKWILL │ │ │ ├── THINKWILL.at │ │ │ ├── YUGE │ │ │ ├── YUGE.at │ │ │ ├── ZTE │ │ │ ├── ZTE.at │ │ │ ├── ch_to_band │ │ │ ├── modeminfo │ │ │ └── probeport.gcom │ │ └── snmpmodem.sh │ ├── mrtg/ │ │ ├── Makefile │ │ ├── README.md │ │ └── patches/ │ │ ├── 0001-disable-longlong-fmt.patch │ │ └── 0002-fix-perl-path.patch │ ├── mwan3-ledhelper/ │ │ ├── Makefile │ │ └── root/ │ │ ├── etc/ │ │ │ ├── config/ │ │ │ │ └── mwan3_led │ │ │ └── hotplug.d/ │ │ │ └── iface/ │ │ │ └── 17-mwan3-ledhelper │ │ └── usr/ │ │ └── share/ │ │ └── mwan3_led_config.sh │ ├── n2n/ │ │ └── Makefile │ ├── nat64/ │ │ ├── Makefile │ │ └── patches/ │ │ └── 01-fix-checksum-packet.patch │ ├── natflow/ │ │ ├── Makefile │ │ └── files/ │ │ └── natflow-boot.init │ ├── ndpi-netfilter/ │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ └── patches/ │ │ ├── 0001-outline-atomics-skbuff-check.patch │ │ ├── 0002-fix-build-murmurhash.patch │ │ └── 0003-fix-xchg.patch │ ├── ookla-speedtest/ │ │ ├── Makefile │ │ └── files/ │ │ ├── aarch64/ │ │ │ └── speedtest │ │ ├── armhf/ │ │ │ └── speedtest │ │ ├── i386/ │ │ │ └── speedtest │ │ └── x86_64/ │ │ └── speedtest │ ├── openvpn-dns-hotplug/ │ │ ├── Makefile │ │ └── root/ │ │ └── etc/ │ │ └── hotplug.d/ │ │ └── openvpn/ │ │ └── 90-dns │ ├── pingcontrol/ │ │ ├── Makefile │ │ └── files/ │ │ ├── pingcontrol │ │ ├── pingcontrol.config │ │ └── pingcontrol.init │ ├── rrm-nr-distributor/ │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── example_wireless_config │ │ └── root/ │ │ ├── etc/ │ │ │ └── init.d/ │ │ │ └── rrm_nr │ │ └── usr/ │ │ └── bin/ │ │ └── rrm_nr │ ├── simpleproxy/ │ │ └── Makefile │ ├── ssw/ │ │ ├── Makefile │ │ └── root/ │ │ ├── etc/ │ │ │ ├── config/ │ │ │ │ └── ssw │ │ │ └── init.d/ │ │ │ └── ssw │ │ └── usr/ │ │ └── share/ │ │ └── ssw_track.sh │ ├── telegrambot/ │ │ ├── Makefile │ │ ├── README.md │ │ └── root/ │ │ ├── etc/ │ │ │ ├── config/ │ │ │ │ └── telegrambot │ │ │ └── init.d/ │ │ │ └── telegrambot │ │ └── usr/ │ │ └── lib/ │ │ └── telegrambot/ │ │ ├── plugins/ │ │ │ ├── functions/ │ │ │ │ ├── get_mac.sh │ │ │ │ └── ping.sh │ │ │ ├── ifconfig.sh │ │ │ ├── kernel.sh │ │ │ ├── leases.sh │ │ │ ├── memory.sh │ │ │ ├── netstat.sh │ │ │ ├── opkg.sh │ │ │ ├── ping.sh │ │ │ ├── plugins.sh │ │ │ ├── swports_list.sh │ │ │ ├── uci.sh │ │ │ ├── uptime.sh │ │ │ ├── wanip.sh │ │ │ ├── wifi_list.sh │ │ │ ├── wll_list.sh │ │ │ └── wol.sh │ │ └── telegrambot │ ├── teleproxy/ │ │ ├── Makefile │ │ ├── README.md │ │ ├── files/ │ │ │ ├── teleproxy-config-refresh.sh │ │ │ ├── teleproxy.config │ │ │ ├── teleproxy.defaults │ │ │ └── teleproxy.init │ │ └── patches/ │ │ ├── 0001-makefile.patch │ │ ├── 0002-fix-mips-variable.patch │ │ ├── 0003-rtds-alternate.patch │ │ ├── 0004-atomic-32bit-emu.patch │ │ ├── 0005-makefile-atomic-emu.patch │ │ └── 0006-fix-mips-musl-sigrtmax.patch │ ├── torrserver/ │ │ ├── Makefile │ │ └── files/ │ │ ├── torrserver.config │ │ └── torrserver.init │ ├── totd/ │ │ ├── Makefile │ │ └── root/ │ │ └── etc/ │ │ ├── config/ │ │ │ └── totd │ │ └── init.d/ │ │ └── totd │ ├── tun2socks/ │ │ ├── Makefile │ │ ├── README.md │ │ └── root/ │ │ └── lib/ │ │ └── netifd/ │ │ └── proto/ │ │ └── t2s.sh │ ├── xmm-modem/ │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ └── root/ │ │ ├── etc/ │ │ │ ├── gcom/ │ │ │ │ ├── fm350-auth.gcom │ │ │ │ ├── fm350-config.gcom │ │ │ │ ├── fm350-connect.gcom │ │ │ │ ├── probeport.gcom │ │ │ │ ├── xmm-auth.gcom │ │ │ │ ├── xmm-config.gcom │ │ │ │ ├── xmm-connect.gcom │ │ │ │ └── xmm-disconnect.gcom │ │ │ └── hotplug.d/ │ │ │ └── usb/ │ │ │ └── 01_xmm.sh │ │ └── lib/ │ │ └── netifd/ │ │ └── proto/ │ │ └── xmm.sh │ ├── xt-tls/ │ │ └── Makefile │ └── xtables-wgobfs/ │ └── Makefile ├── telephony/ │ ├── asterisk-chan-quectel/ │ │ ├── Makefile │ │ └── patches/ │ │ └── 100-openwrtIconvMysql.patch │ ├── atinout/ │ │ └── Makefile │ ├── hl-atcmd/ │ │ └── Makefile │ ├── imei_generator/ │ │ ├── Makefile │ │ └── src/ │ │ └── imei_generator.c │ ├── qtools/ │ │ ├── Makefile │ │ ├── README.md │ │ └── patches/ │ │ ├── 0001-makefile-install.patch │ │ └── 0002-fix-qbadblock-int.patch │ └── ttymux/ │ ├── Makefile │ └── patches/ │ └── 0001-makefile.patch └── utils/ ├── flash-fox/ │ └── Makefile ├── qmbimat/ │ ├── Makefile │ └── patches/ │ ├── 0001-cc.patch │ └── 0002-qmbimat_more_fixes.patch └── qminfo/ ├── Makefile └── src/ ├── Makefile └── qminfo.c ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitmodules ================================================ [submodule "luci/applications/luci-app-gpoint"] path = luci/applications/luci-app-gpoint url = https://github.com/Kodo-kakaku/luci-app-gpoint.git branch = main [submodule "luci/applications/luci-app-cpu-perf"] path = luci/applications/luci-app-cpu-perf url = https://github.com/koshev-msk/luci-app-cpu-perf.git branch = master ================================================ FILE: README.md ================================================ # modemfeed Is a repository for OpenWrt firmware worked by with LTE cellular modems. Included next packages: |Package | Dependies | Description | |:-------------|:----------------------|:-------------------------| | luci-app-modeminfo|modeminfo|Dashboard for LTE modemds.| |luci-app-smstools3|smstools3|web UI smstools3 package.| |luci-app-mmconfig|modemmanager|band manipulation modem via mmcli utility.| |luci-app-atinout|atinout|AT commands tool.| |luci-app-cellled|cellled|LED cellular signal signal strength.| |luci-app-ttl|luci-app-firewall|TTL modification utility with support for both iptables and nftables.| |qminfo|libqmi|simple info from Qualcomm modem chipsets| |qtools|libc|tools manipulation Qualcomm chipset cellualr modems.| |asterisk-chan-quectel|asterisk|asterisk plugin for SimCom and Quectel modems.| |xmm-modem|kmod-usb-net-ncm, kmod-usb-acm|Intel XMM modem connect scripts| * and more packages not included in official OpenWrt Repo. # How-to add repo and compile packages Add next line to feeds.conf.default in OpenWrt SDK/Buildroot ``` src-git modemfeed https://github.com/koshev-msk/modemfeed.git ``` Update feeds and compile singe package ``` ./scripts/feeds update -a; ./scripts/feeds install -a make -j$((`nproc` + 1)) package/feeds/modemfeed//compile ``` or `make menuconfig` menu to include package(s) firmware in Buildroot # Precompiled packages 21,23,24,25 branch http://openwrt.132lan.ru/packages/ ================================================ FILE: luci/applications/luci-app-3proxy/Makefile ================================================ include $(TOPDIR)/rules.mk LUCI_TITLE:=3proxy simple webUI LUCI_DEPENDS:=+3proxy PKG_LICENSE:=GPLv3 PKG_VERSION:=0.1.1 PKG_RELEASE:=2 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-3proxy/README.md ================================================ # luci-app-3proxy Simple webUI OpenWrt Luci for 3proxy ================================================ FILE: luci/applications/luci-app-3proxy/htdocs/luci-static/resources/view/proxy/3proxy.js ================================================ 'use strict'; 'require fs'; 'require view'; 'require ui'; 'require uci'; return view.extend({ load: function() { return uci.load('3proxy'); }, render: function() { var configPath = '/etc/3proxy.cfg'; uci.sections('3proxy', '3proxy').forEach(function(s) { if (s['.type'] === '3proxy' && s.config) { configPath = s.config; } }); this.configPath = configPath; var container = E('div', { 'class': 'cbi-map' }, [ E('h2', {}, _('3proxy Configuration')), E('div', { 'class': 'cbi-section' }, [ E('div', { 'class': 'cbi-section-descr' }, [ _('Edit 3proxy configuration file.'), E('br'), _('Config file: '), E('code', {}, configPath) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title', 'style': 'display: block; font-weight: bold;' }, _('Configuration')), E('br'), E('br'), E('textarea', { 'class': 'cbi-input-textarea', 'rows': 20, 'style': 'width: 100%; box-sizing: border-box;', 'name': 'config', 'id': 'config-textarea' }, '') ]), E('div', { 'class': 'cbi-value', 'style': 'display: flex; justify-content: flex-end; margin-top: 20px; gap: 10px;' }, [ E('div', { 'class': 'cbi-value-field right' }, [ E('button', { 'class': 'cbi-button cbi-button-save', 'click': ui.createHandlerFn(this, 'saveConfig') }, _('Save')), ' ', E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(this, 'restartService') }, _('Restart 3proxy')) ]) ]) ]) ]); var self = this; fs.read(configPath).catch(function(err) { return ''; }).then(function(content) { var textarea = document.getElementById('config-textarea'); if (textarea) { textarea.value = content || ''; } }); return container; }, saveConfig: function() { var textarea = document.querySelector('textarea[name="config"]'); if (!textarea) { textarea = document.getElementById('config-textarea'); } var config = textarea.value.trim().replace(/\r\n/g, '\n') + '\n'; var configPath = this.configPath || '/etc/3proxy.cfg'; return fs.write(configPath, config).then(function() { ui.addNotification(null, E('p', _('Configuration saved successfully')), 'info'); }).catch(function(err) { ui.addNotification(null, E('p', _('Error saving configuration: ') + err.message), 'error'); }); }, restartService: function() { return fs.exec('/etc/init.d/3proxy', ['restart']).then(function(res) { if (res.code === 0) { ui.addNotification(null, E('p', _('3proxy restarted successfully')), 'info'); } else { ui.addNotification(null, E('p', _('Error restarting 3proxy: ') + res.stderr), 'error'); } }).catch(function(err) { ui.addNotification(null, E('p', _('Error restarting 3proxy: ') + err.message), 'error'); }); }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-3proxy/po/ru/3proxy.po ================================================ "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlakov \n" msgid "Configuration" msgstr "Настройки" msgid "3proxy Configuration" msgstr "Файл настройки 3proxy." msgid "Config file: " msgstr "Файл настройки: " msgid "Edit 3proxy configuration file." msgstr "Редактируйте файл настройки." msgid "Save" msgstr "Сохранить" msgid "Restart 3proxy" msgstr "Перезапустить 3proxy" msgid "Configuration saved successfully" msgstr "Конфигурация сохранена" msgid "Error saving configuration: " msgstr "Ошибка сохранения: " msgid "3proxy restarted successfully" msgstr "3proxy успешно перезапущен" msgid "Error restarting 3proxy: " msgstr "Ошибка запуска 3proxy: " ================================================ FILE: luci/applications/luci-app-3proxy/root/usr/share/luci/menu.d/luci-app-3proxy.json ================================================ { "admin/services/3proxy": { "title": "3proxy", "action": { "type": "alias", "path": "admin/services/3proxy/config" }, "depends": { "acl": [ "luci-app-3proxy" ], "uci": { "3proxy": true } } }, "admin/services/3proxy/config": { "title": "Config", "order": 41, "action": { "type": "view", "path": "proxy/3proxy" } } } ================================================ FILE: luci/applications/luci-app-3proxy/root/usr/share/rpcd/acl.d/luci-app-3proxy.json ================================================ { "luci-app-3proxy": { "description": "Grant UCI access for luci-app-3proxy", "read": { "file": { "/etc/3proxy.cfg": [ "read" ], "/etc/init.d/3proxy": [ "exec" ] }, "uci": [ "3proxy" ] }, "write": { "file": { "/etc/3proxy.cfg": [ "write" ] }, "uci": [ "3proxy" ] } } } ================================================ FILE: luci/applications/luci-app-atinout/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: luci/applications/luci-app-atinout/Makefile ================================================ # # Copyright 2022-2023 Rafał Wabik - IceG - From eko.one.pl forum # # Licensed to the GNU General Public License v3.0. # include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-atinout LUCI_TITLE:=LuCI JS Support for AT commands MAINTAINER:=Rafał Wabik <4Rafal@gmail.com> LUCI_DESCRIPTION:=LuCI JS interface for the atinout. LUCI_DEPENDS:=+atinout LUCI_PKGARCH:=all PKG_VERSION:=0.1.1 PKG_RELEASE:=5 define Package/$(PKG_NAME)/conffiles /etc/atcommands.user endef include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-atinout/README.md ================================================ # luci-app-atinout Web UI AT-commands using atinout for OpenWrt. ![](https://raw.githubusercontent.com/4IceG/Personal_data/master/atcommands.gif) ================================================ FILE: luci/applications/luci-app-atinout/htdocs/luci-static/resources/view/modem/atcommands.js ================================================ 'use strict'; 'require dom'; 'require form'; 'require fs'; 'require ui'; 'require view'; /* Copyright 2022-2023 Rafał Wabik - IceG - From eko.one.pl forum Modified for atinout by Konstantine Shevlakov 2025 Licensed to the GNU General Public License v3.0. */ return view.extend({ handleCommand: function(exec, args) { var buttons = document.querySelectorAll('.cbi-button'); for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('disabled', 'true'); return fs.exec(exec, args).then(function(res) { var out = document.querySelector('.atcommand-output'); out.style.display = ''; res.stdout = res.stdout?.trim().replace(/\n\s*\n\s*\n+/g, '\n\n') || ''; res.stderr = res.stderr?.trim().replace(/\n\s*\n\s*\n+/g, '\n\n') || ''; dom.content(out, [ res.stdout || '', res.stderr || '' ]); }).catch(function(err) { ui.addNotification(null, E('p', [ err ])) }).finally(function() { for (var i = 0; i < buttons.length; i++) buttons[i].removeAttribute('disabled'); }); }, handleGo: function(ev) { var atcmd = document.getElementById('cmdvalue').value; var portSelect = document.getElementById('portselect'); var port = portSelect ? portSelect.value : ''; if (atcmd.length < 2) { ui.addNotification(null, E('p', _('Please specify the AT command to send')), 'info'); return false; } if (!port) { ui.addNotification(null, E('p', _('Please select a port for communication with the modem')), 'info'); return false; } return this.handleCommand('luci-app-atinout', [atcmd, port]); }, handleClear: function(ev) { var out = document.querySelector('.atcommand-output'); out.style.display = 'none'; var ov = document.getElementById('cmdvalue'); ov.value = ''; document.getElementById('cmdvalue').focus(); }, handleCopy: function(ev) { var ov = document.getElementById('cmdvalue'); ov.value = ''; var x = document.getElementById('tk').value; ov.value = x; }, // search port in /dev/ dir scanPorts: function() { return fs.list('/dev').then(function(devices) { var ports = []; if (devices) { devices.forEach(function(device) { var name = device.name; if (name) { if (name.startsWith('ttyUSB') || name.startsWith('ttyACM') || /^wwan\d+at\d+/.test(name)) { ports.push('/dev/' + name); } } }); } ports.sort(); return ports; }).catch(function(err) { console.error('Error scanning ports:', err); return []; }); }, updatePortList: function() { var self = this; var portSelect = document.getElementById('portselect'); if (!portSelect) return; portSelect.disabled = true; var currentValue = portSelect.value; this.scanPorts().then(function(ports) { var currentPort = currentValue; while (portSelect.firstChild) { portSelect.removeChild(portSelect.firstChild); } var emptyOption = E('option', { value: '' }, _('-- Select port --')); portSelect.appendChild(emptyOption); ports.forEach(function(port) { var option = E('option', { value: port }, port); if (port === currentPort) { option.setAttribute('selected', 'selected'); } portSelect.appendChild(option); }); portSelect.disabled = false; // if ports not found if (ports.length === 0) { ui.addNotification(null, E('p', _('No ttyUSB or ttyACM ports found')), 'info'); } }); }, load: function() { return Promise.all([ L.resolveDefault(fs.read_direct('/etc/atcommands.user'), null) ]); }, render: function (loadResults) { var info = _('User interface for handling AT commands using atinout utility.'); var self = this; var container = E('div', { 'class': 'cbi-map', 'id': 'map' }, [ E('h2', {}, [ _('AT Commands') ]), E('div', { 'class': 'cbi-map-descr'}, info), E('hr'), E('div', { 'class': 'cbi-section' }, [ E('div', { 'class': 'cbi-section-node' }, [ E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('User AT commands') ]), E('div', { 'class': 'cbi-value-field' }, [ E('select', { 'class': 'cbi-input-select', 'id': 'tk', 'style': 'margin:5px 0; width:100%;', 'change': ui.createHandlerFn(this, 'handleCopy') }, (loadResults[0] || "").trim().split("\n").map(function(cmd) { var fields = cmd.split(/;/); var name = fields[0]; var code = fields[1]; return E('option', { 'value': code }, name ); }) ) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('Modem Port') ]), E('div', { 'class': 'cbi-value-field' }, [ E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [ E('select', { 'class': 'cbi-input-select', 'id': 'portselect', 'style': 'margin:5px 0; width: 100%;', 'name': 'port' }, [ E('option', { value: '' }, _('-- Scanning ports --')) ]), E('button', { 'class': 'cbi-button cbi-button-action', 'style': 'white-space: nowrap;', 'click': ui.createHandlerFn(this, function() { this.updatePortList(); }) }, [ _('Refresh') ]) ]) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('Command to send') ]), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'style': 'margin:5px 0; width:100%;', 'type': 'text', 'id': 'cmdvalue', 'data-tooltip': _('Press [Enter] to send the command, press [Delete] to delete the command'), 'keydown': function(ev) { if (ev.keyCode === 13) { var execBtn = document.getElementById('execute'); if (execBtn) execBtn.click(); } if (ev.keyCode === 46) { var del = document.getElementById('cmdvalue'); if (del) { var ov = document.getElementById('cmdvalue'); ov.value = ''; document.getElementById('cmdvalue').focus(); } } } }), ]) ]), ]) ]), E('hr'), E('div', { 'class': 'right' }, [ E('button', { 'class': 'cbi-button cbi-button-remove', 'id': 'clr', 'click': ui.createHandlerFn(this, 'handleClear') }, [ _('Clear form') ]), '\xa0\xa0\xa0', E('button', { 'class': 'cbi-button cbi-button-action important', 'id': 'execute', 'click': ui.createHandlerFn(this, 'handleGo') }, [ _('Send command') ]), ]), E('p', _('Reply')), E('pre', { 'class': 'atcommand-output', 'id': 'preout', 'style': 'display:none; border: 1px solid var(--border-color-medium); border-radius: 5px; font-family: monospace' }), ]); setTimeout(function() { self.updatePortList(); }, 100); return container; }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-atinout/htdocs/luci-static/resources/view/modem/atconfig.js ================================================ 'use strict'; 'require fs'; 'require view'; 'require ui'; 'require rpc'; /* Copyright 2022-2023 Rafał Wabik - IceG - From eko.one.pl forum Modified for atinout by Konstantine Shevlakov 2023 Licensed to the GNU General Public License v3.0. */ var cmddesc = _("Each line must have the following format: AT command description;AT+COMMAND.
For user convenience, the file is saved to the location /etc/atcommands.user."); return view.extend({ load: function() { return fs.read('/etc/atcommands.user').catch(function(err) { return ''; }); }, render: function(content) { var content = content || ''; return E('div', { 'class': 'cbi-map' }, [ E('div', { 'class': 'cbi-map-descr' }, _('AT Commands Configuration')), E('div', { 'class': 'cbi-section' }, [ E('div', { 'class': 'cbi-section-descr' }, cmddesc), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('User AT commands')), E('div', { 'class': 'cbi-value-field' }, [ E('textarea', { 'class': 'cbi-input-textarea', 'rows': 20, 'style': 'width: 100%', 'name': 'atcommands' }, content) ]) ]), E('div', { 'class': 'cbi-value', 'style': 'display: flex; justify-content: flex-end; margin-top: 20px;' }, [ E('div', { 'class': 'cbi-value-field right' }, [ E('button', { 'class': 'cbi-button cbi-button-save', 'click': ui.createHandlerFn(this, 'saveCommands') }, _('Save')) ]) ]) ]) ]); }, saveCommands: function(ev) { var textarea = document.querySelector('textarea[name="atcommands"]'); var commands = textarea.value.trim().replace(/\r\n/g, '\n') + '\n'; return fs.write('/etc/atcommands.user', commands).then(function() { ui.addNotification(null, E('p', _('AT commands list saved successfully')), 'info'); }).catch(function(err) { ui.addNotification(null, E('p', _('Error saving list: ') + err.message), 'error'); }); }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-atinout/po/ru/atinout.po ================================================ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" msgid "AT Commands" msgstr "Команды AT" msgid "Configuration" msgstr "Настройки" msgid "Please specify the AT command to send" msgstr "Выберите команду AT для отправки" msgid "Please set the port for communication with the modem" msgstr "Выберите порт модема." msgid "Please select a port for communication with the modem" msgstr "Выберите порт модема." msgid "User AT commands" msgstr "Пользовательские команды AT" msgid "Command to send" msgstr "Команда к отправке" msgid "Clear form" msgstr "Очистить" msgid "Send command" msgstr "Отправить" msgid "Reply" msgstr "Ответ" msgid "User interface for handling AT commands using atinout utility." msgstr "Интерфейс для отсылки команд AT с использованием atinout." msgid "Configuration atinout" msgstr "Настройки atinout" msgid "Configuration panel for atinout." msgstr "Панель конфигурации atinout" msgid "Modem Port" msgstr "Порт модема" msgid "-- Select port --" msgstr "-- Выберите порт --" msgid "Each line must have the following format: AT command description;AT+COMMAND.
For user convenience, the file is saved to the location /etc/atcommands.user." msgstr "Формат записи команд: Описание команды;Команда AT.
Пользовательские команды содержатся в файле /etc/atcommands.user." msgid "Press [Enter] to send the command, press [Delete] to delete the command" msgstr "Нажмите [Enter] для отправки или [Delete] для удаления" msgid "AT commands list saved successfully" msgstr "Список команд успешно сохранён" msgid "Error saving list: " msgstr "Ошибка сохраниения: " msgid "No ttyUSB or ttyACM ports found" msgstr "Последовательные порты модема не обнаружены" ================================================ FILE: luci/applications/luci-app-atinout/po/template/atinout.pot ================================================ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" msgid "AT Commands" msgstr "" msgid "Configuration" msgstr "" msgid "Please specify the AT command to send" msgstr "" msgid "Please set the port for communication with the modem" msgstr "" msgid "User AT commands" msgstr "" msgid "Command to send" msgstr "" msgid "Clear form" msgstr "" msgid "Send command" msgstr "" msgid "Reply" msgstr "" msgid "User interface for handling AT commands using sms-tool. More information about the sms-tool on the %seko.one.pl forum%s." msgstr "" msgid "Configuration sms-tool" msgstr "" msgid "Configuration panel for sms-tool and gui application." msgstr "" msgid "Port for communication with the modem" msgstr "" msgid "Select one of the available ttyUSBX ports." msgstr "" msgid "Please select a port" msgstr "" msgid "Each line must have the following format: 'At command description;AT command'. For user convenience, the file is saved to the location '/etc/modem/atcommands.user'." msgstr "" msgid "Press [Enter] to send the command, press [Delete] to delete the command." msgstr "" ================================================ FILE: luci/applications/luci-app-atinout/po/zh_Hans/atinout.po ================================================ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" msgid "AT Commands" msgstr "AT命令" msgid "Configuration" msgstr "配置" msgid "Please specify the AT command to send" msgstr "请选择要发送的 AT命令" msgid "Please set the port for communication with the modem" msgstr "请选择与调制解调器通信的端口" msgid "User AT commands" msgstr "用户自定义 AT 命令" msgid "Command to send" msgstr "待发送命令" msgid "Clear form" msgstr "清除表单" msgid "Send command" msgstr "发送命令" msgid "Reply" msgstr "回复" msgid "User interface for handling AT commands using atinout utility." msgstr "使用 atinout工具处理 AT命令的用户界面" msgid "Configuration atinout" msgstr "atinout 的配置" msgid "Configuration panel for atinout." msgstr "atinout 的配置面板" msgid "Port for communication with the modem" msgstr "调制解调器通信端口" msgid "Select serial modem port." msgstr "选择串行调制解调器端口" msgid "Please select a port" msgstr "请选择一个端口" msgid "Each line must have the following format: 'At command description;AT command'. For user convenience, the file is saved to the location /etc/atcommands.user." msgstr "每行应遵循以下格式:'AT命令描述;AT命令'。为了方便用户,文件会被保存在/etc/atcommands.user位置。" msgid "Press [Enter] to send the command, press [Delete] to delete the command" msgstr "按[Enter]键发送命令,按[Delete]键删除命令" ================================================ FILE: luci/applications/luci-app-atinout/root/etc/uci-defaults/65-luci-app-atinout ================================================ #!/bin/sh if [ ! -f /etc/atcommands.user ]; then echo "Demo command \"ATI\";ATI" > /etc/atcommands.user fi rm -rf /tmp/luci-* ================================================ FILE: luci/applications/luci-app-atinout/root/usr/bin/luci-app-atinout ================================================ #!/bin/sh echo $1 | atinout - $2 - ================================================ FILE: luci/applications/luci-app-atinout/root/usr/share/luci/menu.d/luci-app-atinout.json ================================================ { "admin/modem": { "title": "Modem", "order": 45, "action": { "type": "firstchild", "recurse": true } }, "admin/modem/atinout": { "title": "AT Commands", "order": 41, "action": { "type": "alias", "path": "admin/modem/atinout/atcommands" }, "depends": { "acl": [ "luci-app-atinout" ] } }, "admin/modem/atinout/atcommands": { "title": "AT Commands", "order": 41, "action": { "type": "view", "path": "modem/atcommands" } }, "admin/modem/atinout/atconfig": { "title": "Configuration", "order": 42, "action": { "type": "view", "path": "modem/atconfig" } } } ================================================ FILE: luci/applications/luci-app-atinout/root/usr/share/rpcd/acl.d/luci-app-atinout.json ================================================ { "luci-app-atinout": { "description": "Grant access to atcommands executables", "read": { "cgi-io": [ "exec" ], "file": { "/bin/echo": [ "exec" ], "/usr/bin/atinout": [ "exec" ], "/usr/bin/luci-app-atinout": [ "exec" ], "/etc/atcommands.user": [ "read" ] } }, "write": { "file": { "/etc/atcommands.user": [ "write" ] } } } } ================================================ FILE: luci/applications/luci-app-cellled/Makefile ================================================ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-cellled LUCI_DEPENDS:=+cellled PKG_VERSION:=0.1.0 PKG_RELEASE:=2 LUCI_TITLE:=LuCI application for showing cellular RSSI LED include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-cellled/htdocs/luci-static/resources/view/cellled.js ================================================ 'use strict'; 'require form'; 'require view'; 'require uci'; 'require rpc'; 'require fs'; 'require tools.widgets as widgets'; function getModemList() { return fs.exec_direct('/usr/bin/mmcli', [ '-L' ]).then(function(res) { var lines = (res || '').split(/\n/), tasks = []; for (var i = 0; i < lines.length; i++) { var m = lines[i].match(/\/Modem\/(\d+)/); if (m) tasks.push(fs.exec_direct('/usr/bin/mmcli', [ '-m', m[1] ])); } return Promise.all(tasks).then(function(res) { var modems = []; for (var i = 0; i < res.length; i++) { var man = res[i].match(/manufacturer: ([^\n]+)/), mod = res[i].match(/model: ([^\n]+)/), dev = res[i].match(/device: ([^\n]+)/); if (dev) { modems.push({ device: dev[1].trim(), manufacturer: (man ? man[1].trim() : '') || '?', model: (mod ? mod[1].trim() : '') || dev[1].trim() }); } } return modems; }); }); } var callSerialPort = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ], expect: { entries: [] }, filter: function(list, params) { var rv = []; for (var i = 0; i < list.length; i++) if (list[i].name.match(/^ttyACM/) || list[i].name.match(/^ttyUSB/)) rv.push(params.path + list[i].name); return rv.sort(); } }); var callQMIPort = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ], expect: { entries: [] }, filter: function(list, params) { var rv = []; for (var i = 0; i < list.length; i++) if (list[i].name.match(/^cdc-wdm/)) rv.push(params.path + list[i].name); return rv.sort(); } }); var callLEDs = rpc.declare({ object: 'luci', method: 'getLEDs', expect: { '': {} } }); return view.extend({ load: function() { return Promise.all([ callLEDs(), L.resolveDefault(fs.list('/www' + L.resource('view/system/led-trigger')), []), ]).then(function(data) { var plugins = data[1]; var tasks = []; for (var i = 0; i < plugins.length; i++) { var m = plugins[i].name.match(/^(.+)\.js$/); if (plugins[i].type != 'file' || m == null) continue; tasks.push(L.require('view.system.led-trigger.' + m[1]).then(L.bind(function(name){ return L.resolveDefault(L.require('view.system.led-trigger.' + name)).then(function(form) { return { name: name, form: form, }; }); }, this, m[1]))); } return Promise.all(tasks).then(function(plugins) { var value = {}; value[0] = data[0]; value[1] = plugins; return value; }); }); }, render: function(data) { var m, s, o, triggers = []; var leds = data[0]; var plugins = data[1]; for (var k in leds) for (var i = 0; i < leds[k].triggers.length; i++) triggers[i] = leds[k].triggers[i]; m = new form.Map('cellled', _('CellLED')); m.description = _('Application for showing cellular RSSI LEDs'); s = m.section(form.TypedSection, 'device', _('General setup')); s.anonymous = true; s.rmempty = true; o = s.option(form.ListValue, 'data_type', _('Select Data service')); o.value('qmi', 'libQMI'); o.value('uqmi', 'uQMI'); o.value('mm', 'Modem Manager'); o.value('serial', 'Serial Port'); o = s.option(form.ListValue, 'device', _('Select Data port')); o.load = function(section_id) { return callSerialPort('/dev/').then(L.bind(function(devices) { for (var i = 0; i < devices.length; i++) this.value(devices[i]); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o.rmempty = true; o.depends('data_type', 'serial'); o = s.option(form.ListValue, 'device_qmi', _('Select Data port')); o.load = function(section_id) { return callQMIPort('/dev/').then(L.bind(function(devices) { for (var i = 0; i < devices.length; i++) this.value(devices[i]); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o.rmempty = true; o.depends('data_type', 'qmi'); o.depends('data_type', 'uqmi'); o = s.option(form.ListValue, 'device_mm', _('Select Data port')); o.load = function(section_id) { return getModemList().then(L.bind(function(devices) { for (var i = 0; i < devices.length; i++) this.value(devices[i].device, '%s - %s'.format(devices[i].manufacturer, devices[i].model)); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o.rmempty = true; o.depends('data_type', 'mm'); o = s.option(form.Value, 'timeout', _('Timeout interval data(sec)')); o.datatype = 'and(uinteger,min(5))'; o = s.option(form.Flag, 'rgb_led', _('RGB LED'), _('Use RGB Led')); o = s.option(form.Flag, 'use_pwm', _('Use PWM'), _('Enable if Support PWM LED')); o.depends('rgb_led', '1'); o = s.option(form.ListValue, 'red_led', _('Red LED')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.rmempty = true; o.depends('rgb_led', '1'); o = s.option(form.ListValue, 'green_led', _('Greed LED')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.rmempty = true; o.depends('rgb_led', '1'); o = s.option(form.ListValue, 'blue_led', _('Blue LED')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.rmempty = true; o.depends('rgb_led', '1'); s = m.section(form.GridSection, 'rssi_led', _('Signal strength values')); s.addremove = true; s.anonymous = true; s.nodescriptions = true; o = s.option(form.Flag, 'rgb', _('RGB Led')); o = s.option(form.ListValue, 'led', _('LED')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.rmempty = true; o.depends('rgb', '0'); o = s.option(form.ListValue, 'type', _('Quality')); o.value('poor',_('Poor')); o.value('bad',_('Bad')); o.value('fair',_('Fair')); o.value('good',_('Good')); o.depends('rgb', '1'); o = s.option(form.Value, 'rssi_min', _('Min.value %')); o.datatype = 'and(uinteger,min(0),max(100))'; o.rmempty = true; o = s.option(form.Value, 'rssi_max', _('Max.value %')); o.datatype = 'and(uinteger,min(0),max(100))'; o.rmempty = true; return m.render(); } }); ================================================ FILE: luci/applications/luci-app-cellled/po/ru/cellled.po ================================================ "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlyakov \n" msgid "Application for showing cellular RSSI LEDs" msgstr "Отображение сигнала сотовой сети RSSI" msgid "Select Data service'" msgstr "Выберите тип данных" msgid "Select Data port" msgstr "Выберите порт данных" msgid "Timeout interval data(sec)" msgstr "Интервал обновления данных (сек)" msgid "Use RGB Led" msgstr "Используется RGB Led" msgid "Enable if Support PWM LED" msgstr "Включить ШИМ, если доступно" msgid "Red LED" msgstr "Красный" msgid "Greed LED" msgstr "Зеленый" msgid "Blue LED" msgstr "Синий" msgid "Signal strength values" msgstr "Значения силы сигнала" msgid "Min.value %" msgstr "Мин.значение %" msgid "Max.value %" msgstr "Макс.значение %" msgid "Quality" msgstr "Качество" msgid "Poor" msgstr "Ничтожное" msgid "Bad" msgstr "Плохое" msgid "Fair" msgstr "Среднее" msgid "Good" msgstr "Хорошее" ================================================ FILE: luci/applications/luci-app-cellled/root/usr/share/luci/menu.d/luci-app-cellled.json ================================================ { "admin/modem": { "title": "Modem", "order": 45, "action": { "type": "firstchild", "rescue": "true" } }, "admin/modem/cellled": { "title": "CellLED", "order": "61", "action": { "type": "view", "path": "cellled" }, "depends": { "uci": { "cellled": true } } } } ================================================ FILE: luci/applications/luci-app-cellled/root/usr/share/rpcd/acl.d/luci-app-cellled.json ================================================ { "luci-app-cellled": { "description": "Grant UCI access for luci-app-cellled", "read": { "file": { "/usr/bin/mmcli": [ "exec" ] }, "uci": [ "cellled" ] }, "write": { "uci": [ "cellled" ] } } } ================================================ FILE: luci/applications/luci-app-mmconfig/LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: luci/applications/luci-app-mmconfig/Makefile ================================================ include $(TOPDIR)/rules.mk LUCI_TITLE:=Configrure modem bands via mmcli utility LUCI_DEPENDS:=+luci-proto-modemmanager +luci-app-modeminfo PKG_LICENSE:=GPLv3 PKG_VERSION:=0.1.1 PKG_RELEASE:=5 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-mmconfig/README.md ================================================ # luci-app-mmconfig The OpenWrt Luci app configure modem cellular bands via mmcli utility ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-mmconfig/screenshot.png) ================================================ FILE: luci/applications/luci-app-mmconfig/htdocs/luci-static/resources/view/modem/bands.js ================================================ 'use strict'; 'require form'; 'require uci'; 'require view'; 'require dom'; 'require modemmanager_helper as helper'; return view.extend({ load: function() { return Promise.all([ uci.load('mmconfig'), helper.getModems() ]); }, render: function(data) { var modemsData = data[1]; var m = new form.Map('mmconfig', _('Modem Configuration'), _('List supported bands.
If deselect all bands, then used default band modem config.')); // add styles var style = document.createElement('style'); style.textContent = this.getCSS(); document.head.appendChild(style); var configSections = []; uci.sections('mmconfig', 'modem', function(s) { configSections.push(s); }); configSections.forEach(function(section, index) { // search modems var modemObj = null; if (modemsData && modemsData.length > 0) { for (var i = 0; i < modemsData.length; i++) { if (modemsData[i] && modemsData[i].modem && modemsData[i].modem.generic && modemsData[i].modem.generic.device === section.device) { modemObj = modemsData[i].modem; break; } } } // create sections var s = m.section(form.NamedSection, section['.name'], _('Modem ') + (index + 1)); s.addremove = false; s.anonymous = false; // hide device option var o = s.option(form.HiddenValue, 'device', ''); o.default = section.device || ''; // container if (modemObj && modemObj.generic) { var infoPanel = s.option(form.DummyValue, '_info_panel', ''); infoPanel.rawhtml = true; var html = '
'; // Operator name modem and access tech var modelText = ''; if (modemObj.generic.manufacturer || modemObj.generic.model) { if (modemObj.generic.manufacturer) { modelText += modemObj.generic.manufacturer + ' '; } if (modemObj.generic.model) { modelText += modemObj.generic.model; } } else { modelText = _('Unknown modem'); } var operatorText = ''; if (modemObj['3gpp'] && modemObj['3gpp']['operator-name']) { operatorText = modemObj['3gpp']['operator-name']; } html += '
'; html += '' + modelText + ''; if (operatorText) { html += ''; html += '' + operatorText + ''; } var currentModeText = ''; if (modemObj.generic['current-modes']) { currentModeText = modemObj.generic['current-modes']; } // if current-modes not aviaible, use access-technologies else if (modemObj.generic['access-technologies'] && modemObj.generic['access-technologies'].length > 0) { // Берем первую технологию из access-technologies currentModeText = modemObj.generic['access-technologies']; } if (currentModeText) { html += ''; html += '' + _('Access Tech:') + ' ' + currentModeText + ''; } html += '
'; html += '
'; // close container infoPanel.default = html; } // network preffered o = s.option(form.ListValue, 'preffer', _('Network Mode')); o.rmempty = false; if (modemObj && modemObj.generic && modemObj.generic['supported-modes']) { // get from modem supported-modes modemObj.generic['supported-modes'].forEach(function(mode) { o.value(mode, mode); }); // Set current if (section.preffer) { o.default = section.preffer; } else if (currentModeText && modemObj.generic['supported-modes'].includes(currentModeText)) { o.default = currentModeText; } } else { // If not aviable o.value('', _('Not Available')); o.default = ''; o.readonly = true; } // bands select if (modemObj && modemObj.generic && modemObj.generic['supported-bands']) { o = s.option(form.MultiValue, 'bands', _('Bands')); // get from modem supported-bands modemObj.generic['supported-bands'].forEach(function(band) { o.value(band, band); }); // Set current if (section.bands) { o.default = section.bands; } } else { o = s.option(form.Value, 'bands', _('Bands')); o.value('', _('Not Available')); o.default = ''; o.readonly = true; } // small separator if (index < configSections.length - 1) { var spacer = s.option(form.DummyValue, '_divider', ''); spacer.default = '
'; spacer.rawhtml = true; } }); if (configSections.length === 0) { var s = m.section(form.NamedSection, 'info', _('WARNING')); s.anonymous = true; var o = s.option(form.DummyValue, '_message', _('Status')); o.default = _('No modem configuration found. Run /etc/init.d/mmconfig start'); o.rawhtml = false; } return m.render(); }, getCSS: function() { return [ '.modem-info-compact {', ' background: #f8fafc;', ' border: 1px solid #e2e8f0;', ' border-radius: 6px;', ' padding: 12px 16px;', ' margin: 15px 0;', '}', '', '.compact-line {', ' display: flex;', ' align-items: center;', ' gap: 10px;', '}', '', '.modem-model {', ' font-weight: 600;', ' color: #2d3748;', ' font-size: 1em;', '}', '', '.separator {', ' color: #a0aec0;', ' font-weight: 300;', '}', '', '.modem-operator {', ' color: #4a5568;', ' font-size: 0.95em;', '}', '', '.light-divider {', ' height: 1px;', ' background: #edf2f7;', ' margin: 20px 0;', '}', '', '/* Улучшаем отступы в секциях */', '.cbi-section .cbi-section-node {', ' margin-bottom: 10px;', '}', '', '.cbi-section .cbi-section-descr {', ' padding: 5px 0;', '}' ].join('\n'); } }); ================================================ FILE: luci/applications/luci-app-mmconfig/po/ru/mmconfig.po ================================================ msgid "" msgstr "" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlakov \n" msgid "Modem " msgstr "Модем " msgid "Bands" msgstr "Диапазоны" msgid "Network Mode" msgstr "Режим сети" msgid "Bands" msgstr "Диапазоны" msgid "Access Tech:" msgstr "Текущий:" msgid "Status" msgstr "Состояние" msgid "WARNING" msgstr "ПРЕДУПРЕЖДЕНИЕ" msgid "List supported bands.
If deselect all bands, then used default band modem config." msgstr "Список поддерживаемых диапазонов.
Если не выбрано ни одного диапазона, используется конфигурация модема по умолчанию." msgid "No modem configuration found. Run /etc/init.d/mmconfig start" msgstr "Не найдена конфигурация. Перезапустите скрипт /etc/init.d/mmconfig start" ================================================ FILE: luci/applications/luci-app-mmconfig/po/zh_Hans/mmconfig.po ================================================ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" #: applications/luci-app-mmconfig/luasrc/controller/modemconfig.lua:8 msgid "Modem" msgstr "移动网络" #: applications/luci-app-mmconfig/luasrc/controller/modemconfig.lua:9 msgid "Band config" msgstr "频段配置" #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:12 msgid "Configure modem bands" msgstr "配置模块频段" #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:13 msgid "Configuration 2G/3G/4G modem frequency bands." msgstr "配置 2G/3G/4G 模块频段." #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:13 msgid "Choose bands cellular modem" msgstr "选择模块频段" #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:19 msgid "Net Mode" msgstr "网络模式" #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:19 msgid "Preffered Network mode select." msgstr "首选网络" #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:61 msgid "Select modem" msgstr "选择模块" #: applications/luci-app-mmconfig/luasrc/model/cbi/modem/modemconfig.lua:72 msgid "Maybe must reconnect cellular interface.
If deselect all bands, then used default band modem config." msgstr "可能需要重连移动网络接口。
如果不选择频段则使用模块默认频段。" ================================================ FILE: luci/applications/luci-app-mmconfig/root/etc/config/mmconfig ================================================ ================================================ FILE: luci/applications/luci-app-mmconfig/root/etc/init.d/mmconfig ================================================ #!/bin/sh /etc/rc.common START=99 USE_PROCD=1 check_modems(){ for m in $(mmcli -L | awk '{print $1}'); do devm=$(mmcli -m ${m} -J | jsonfilter -e '@["modem"]["generic"]["device"]') uci show mmconfig | grep "$devm" || { echo "uci section not found" secname=$(uci show mmconfig | tail -1 | awk -F [.] '{num = $2; gsub(/[^0-9]/, "", num); if(num != "") printf "%s%d\n", substr($2, 1, length($2)-length(num)), num+1; else print $0 "1"}') [ $secname ] || secname=modem1 uci set mmconfig.${secname}=modem uci set mmconfig.${secname}.device=$devm uci commit } done } start_service(){ logger -t "MMconfig" "MMConfig started" # generate config uci check_modems } reload_service(){ /usr/share/modeminfo/mmconfig } service_triggers() { procd_add_reload_trigger mmconfig } ================================================ FILE: luci/applications/luci-app-mmconfig/root/etc/uci-defaults/70_luci-app-mmconfig ================================================ #!/bin/sh # disable mode setting to any in modemnanager proto [ -f /lib/netifd/proto/modemmanager.sh ] && { sed -i '/^[[:space:]]*modemmanager_set_allowed_mode "$device" "$interface" "any"$/s/.*/\t\t:/' /lib/netifd/proto/modemmanager.sh } ================================================ FILE: luci/applications/luci-app-mmconfig/root/usr/share/luci/menu.d/luci-app-mmconfig.json ================================================ { "admin/modem/main": { "title": "Modeminfo", "order": 10, "action": { "type": "alias", "path": "admin/modem/main/main" }, "depends": { "acl": [ "luci-app-mmconfig" ], "uci": { "mmconfig": true } } }, "admin/modem/main/bands": { "title": "Bands", "order": 63, "action": { "type": "view", "path": "modem/bands" } } } ================================================ FILE: luci/applications/luci-app-mmconfig/root/usr/share/modeminfo/mmconfig ================================================ #!/bin/sh . /lib/functions.sh # get vars get_mm_config(){ local device bands preffer config_get device $1 device config_get bands $1 bands config_get preffer $1 preffer mmcli -m $device > /dev/null 2>&1 && { setpref=$(echo "$preffer" | awk -F'[:;]' '{ gsub(/[ \t]+/, "", $0); gsub(",", "|", $0); print "--set-"$1"-modes="$2" --set-"$3"-mode="$NF}') setbands=$(echo $bands | awk -v OFS='|' '{$1=$1}1') defbands=$(mmcli -J -m $device | jsonfilter -e '@["modem"]["generic"]["supported-bands"].*' | awk '{bands = (NR==1 ? $1 : bands "|"$1)} END {print bands}') curbands=$(mmcli -J -m $device | jsonfilter -e '@["modem"]["generic"]["current-bands"].*' | awk '{bands = (NR==1 ? $1 : bands "|"$1)} END {print bands}') curpref=$(mmcli -J -m $device | jsonfilter -e '@["modem"]["generic"]["current-modes"]') [ -n "$bands" ] || { setbands=$defbands } [ "$curbands" = "$setbands" ] && [ "$preffer" = "$curpref" ] || { modemif=$(uci show network | grep "$device" | awk -F [.] '{print $2}') [ "$preffer" = "$curpref" ] || mmcli -m $device $setpref [ "$curbands" = "$setbands" ] || mmcli -m $device --set-current-bands="$setbands" sleep 3 ifup $modemif } } } config_load mmconfig config_foreach get_mm_config modem ================================================ FILE: luci/applications/luci-app-mmconfig/root/usr/share/rpcd/acl.d/luci-app-mmconfig.json ================================================ { "luci-app-mmconfig": { "description": "Grant access to mmconfig configuration", "read": { "file": { "/etc/init.d/mmconfig": [ "exec" ] }, "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "changes", "get" ] }, "uci": [ "mmconfig" ] }, "write": { "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "add", "apply", "confirm", "delete", "order", "rename", "set" ] }, "uci": [ "mmconfig" ] } } } ================================================ FILE: luci/applications/luci-app-modeminfo/Makefile ================================================ include $(TOPDIR)/rules.mk LUCI_TITLE:=Information dashboard for 3G/LTE dongle LUCI_DEPENDS:=+modeminfo PKG_LICENSE:=GPLv3 PKG_VERSION:=0.4.7 PKG_RELEASE:=3 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-modeminfo/README.md ================================================ # luci-app-modeminfo 3G/LTE dongle information for OpenWrt LuCi luci-app-modeminfo is fork from https://github.com/IceG2020/luci-app-3ginfo Supported devices: - Quectel EC200T/EC21/EC25/EP06/EM12/EM160R-GL - SimCom SIM7600E-H/SIM7906/A7600/A7906-M2 - STYX MG8224 - Huawei E3372 (LTE)/MU709/ME909 - Sierra Wireless EM7455/EM9191 - HP LT4220 - Dell DW5821e/DW5829e - MEIGLink SLM750-R2/SLM820/SLM828 - MikroTik R11e-LTE/R11e-LTE6 - Fibocom NL668/NL678/L850/L860/NL952/FM150/FM190/FM350 - Gosuncnwelink GM510 - ThinkWill ML7820+ - Yuge CLM920 - ZTE MF823/MF823D
Package contents: |Package |Description | |:-------|:-----------| |luci-app-modeminfo |LuCI web interface | |modeminfo |common files | |modeminfo-qmi |Qualcomm MSM Interface support | |modeminfo-serial-quectel |Quectel modems support | |modeminfo-serial-telit |Telit LN940 (HP LT4220) modem support | |modeminfo-serial-huawei |Huawei MU709/ME909/E3372(stick mode, LTE only) modems support| |modeminfo-serial-sierra |Sierra EM7455/EM9191 modem support | |modeminfo-serial-simcom |SimCOM modems support | |modeminfo-serial-dell |Dell DW5821e/DW5829e modem support | |modeminfo-serial-fibocom |Fibocom LN668/NL678/NL952/FM150/FM190 modems support | |modeminfo-serial-xmm |Fibocom L850/L860/FM350 modems support | |modeminfo-serial-mikrotik |MikroTik R11e-LTE/R11e-LTE6 modems support | |modeminfo-serial-gosun |Gosuncnwelink GM510 support | |modeminfo-serial-tw |ThinkWill ML7820+ support | |modeminfo-serial-yuge |Yuge CLM920 support | |modeminfo-serial-meig |MEIGLink SLM750-R2/SLM820/SLM828 support | |modeminfo-serial-zte |ZTE MF823/MF823D support | |modeminfo-serial-styx |STYX MG8224 support |
Screenshots * Overview page. Short network info. ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-modeminfo/screenshots/modeminfo-overview.png) * Modeminfo index page. Verbose network info. ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-modeminfo/screenshots/modeminfo-network.png) * Modeminfo hardware page. ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-modeminfo/screenshots/modeminfo-hardware.png) * Modeminfo setup page. ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-modeminfo/screenshots/modeminfo-setup.png)
================================================ FILE: luci/applications/luci-app-modeminfo/htdocs/luci-static/resources/modem-indicator.js ================================================ 'use strict'; 'require ui'; 'require poll'; 'require fs'; 'require uci'; function getSignalColor(percent) { if (percent === null || percent === undefined || percent < 5) { return '#000000'; } let p = percent; if (p >= 80) return '#00cc00'; if (p >= 67) { const ratio = (p - 50) / 30; const r = Math.floor(255 * (1 - ratio)); const g = 255; return `rgb(${r}, ${g}, 0)`; } if (p >= 35) { const ratio = (p - 25) / 25; const r = 255; const g = Math.floor(165 + (90 * ratio)); return `rgb(${r}, ${g}, 0)`; } const ratio = (p - 5) / 20; const r = 255; const g = Math.floor(165 * ratio); return `rgb(${r}, ${g}, 0)`; } function getSignalState(percent) { const p = percent || 0; if (p < 10) return 'error'; if (p < 25) return 'warning'; if (p < 50) return 'warning'; return 'active'; } function isUciIndexTwo() { return L.resolveDefault(fs.exec_direct('/sbin/uci', ['get', 'modeminfo.@general[0].index']), '') .then(function(value) { return value.trim() === '2'; }) .catch(function() { return false; }); } function updateIndicator() { isUciIndexTwo().then(function(ok) { if (!ok) { ui.hideIndicator('modem-status'); return; } if (window.location.pathname.includes('/cgi-bin/luci/admin/modem/main')) { ui.hideIndicator('modem-status'); return; } L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem":[]}') .then(function(res) { var data = JSON.parse(res); if (!data.modem || !data.modem.length) { ui.hideIndicator('modem-status'); return; } var parts = data.modem.map(function(modem) { var percent = parseInt(modem.csq_per); var cops = modem.cops || ''; var mode = modem.mode + (parseInt(modem.lteca) > 0 ? '+' : ''); return { mode: mode, cops: cops, percent: percent }; }); var minPercent = Math.min.apply(null, parts.map(function(p) { return p.percent; })); var state = getSignalState(minPercent); var status = parts.map(function(p) { return p.mode + ' ' + p.cops; }).join(' | '); ui.showIndicator('modem-status', status, null, state); var indicator = document.querySelector('[data-indicator="modem-status"]'); if (indicator) { var html = parts.map(function(p) { var color = getSignalColor(p.percent); return ' ' + p.mode + ' ' + p.cops; }).join(' | '); indicator.innerHTML = html; } }) .catch(function() { ui.hideIndicator('modem-status'); }); }); } setTimeout(function() { updateIndicator(); poll.add(updateIndicator, 10); }, 500); return L.Class.extend({ __name__: 'ModemIndicator' }); ================================================ FILE: luci/applications/luci-app-modeminfo/htdocs/luci-static/resources/preload/modem-indicator.js ================================================ 'use strict'; 'require modem-indicator'; return L.Class.extend({ __name__: 'preload.modem-indicator' }); ================================================ FILE: luci/applications/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/hw.js ================================================ 'use strict'; 'require baseclass'; 'require form'; 'require fs'; 'require view'; 'require ui'; 'require uci'; 'require poll'; 'require dom'; 'require tools.widgets as widgets'; /* Copyright Konstantine Shevlakov 2023-2026 Licensed to the GNU General Public License v3.0. Refactored: bug fixes, XSS prevention, deduplication. */ // ─── Helpers ────────────────────────────────────────────────────────────────── /** * Safe getElementById. * @param {string} id * @returns {HTMLElement|null} */ function getEl(id) { return document.getElementById(id); } /** * Write a value into a cell by id. * Falls back to '--' when value is missing/placeholder. * Uses textContent to prevent XSS. * * @param {string} id - Element id * @param {string} value - Raw value from modem JSON * @param {string} [suffix] - Optional suffix appended to a real value (e.g. ' °C') */ function setCell(id, value, suffix) { const el = getEl(id); if (!el) return; const missing = !value || value === '--'; el.textContent = missing ? '--' : value + (suffix || ''); } // ─── Field definitions ──────────────────────────────────────────────────────── // Each entry: { key: modem JSON field, suffix: optional unit string } const MODEM_FIELDS = [ { key: 'device', suffix: '' }, { key: 'firmware', suffix: '' }, { key: 'imsi', suffix: '' }, { key: 'iccid', suffix: '' }, { key: 'imei', suffix: '' }, { key: 'chiptemp', suffix: ' °C' }, ]; // ─── Main view ──────────────────────────────────────────────────────────────── return view.extend({ // FIX: removed unused `data` parameter load: function() { return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}'); }, polldata: poll.add(function() { return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}') .then(function(res) { // FIX: wrapped in try/catch — was crashing on invalid JSON let json; try { json = JSON.parse(res); } catch (e) { console.error('modeminfo hw: JSON parse error', e); return; } if (!json || !Array.isArray(json.modem)) return; for (let i = 0; i < json.modem.length; i++) { const modem = json.modem[i]; // FIX: replaced 6 copy-pasted blocks with a single loop. // FIX: `var view` renamed — was shadowing the LuCI `view` module. // FIX: value '--' now explicitly written to cell (was silently ignored). // FIX: innerHTML → textContent to prevent XSS. // FIX: String.format(x) with no extra args replaced by direct assignment. for (const { key, suffix } of MODEM_FIELDS) { setCell(key + i, modem[key], suffix); } } }); }), render: function(data) { // FIX: wrapped in try/catch — was crashing on invalid JSON let json; try { json = JSON.parse(data); } catch (e) { json = { modem: [] }; } const m = new form.Map('modeminfo', _('Modeminfo: Hardware'), _('Hardware and sim-card info.')); const s = m.section(form.TypedSection, 'general', null); s.anonymous = true; for (let i = 0; i < json.modem.length; i++) { const idx = i + 1; // Pre-build all element ids for this modem slot const ids = {}; for (const { key } of MODEM_FIELDS) { ids[key] = key + i; } let o; if (json.modem.length > 1) { s.tab('modem' + i, _('Modem') + ' ' + idx); o = s.taboption('modem' + i, form.HiddenValue, 'generic'); } else { o = s.option(form.HiddenValue, 'generic'); } o.render = L.bind(function() { return E('div', {}, [ E('h3', { 'class': 'data-tab' }), E('div', { 'class': 'cbi-section' }, [ E('table', { 'class': 'table' }, [ E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('Device')]), E('td', { 'class': 'td left', 'id': ids.device }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('Firmware')]), E('td', { 'class': 'td left', 'id': ids.firmware }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('IMSI')]), E('td', { 'class': 'td left', 'id': ids.imsi }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('ICCID')]), E('td', { 'class': 'td left', 'id': ids.iccid }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('IMEI')]), E('td', { 'class': 'td left', 'id': ids.imei }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('Chiptemp')]), E('td', { 'class': 'td left', 'id': ids.chiptemp }, ['--']), ]), ]) ]) ]); }, this.polldata); o.anonymous = true; o.rmempty = true; } return m.render(); }, handleSaveApply: null, handleSave: null, handleReset: null, }); ================================================ FILE: luci/applications/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/main.js ================================================ 'use strict'; 'require baseclass'; 'require form'; 'require fs'; 'require view'; 'require ui'; 'require uci'; 'require poll'; 'require dom'; 'require tools.widgets as widgets'; /* Copyright Konstantine Shevlakov 2023 Licensed to the GNU General Public License v3.0. Refactored: bug fixes, performance improvements, code deduplication. */ const REGISTERED_STATUSES = [1, 6, 9]; const ROAMING_STATUSES = [3, 5, 7, 10]; // ─── Signal Icons ──────────────────────────────────────────────────────────── const SIGNAL_ICONS = [ { max: 10, icn: 'signal-000-000.svg' }, { max: 25, icn: 'signal-000-025.svg' }, { max: 50, icn: 'signal-025-050.svg' }, { max: 75, icn: 'signal-050-075.svg' }, { max: Infinity, icn: 'signal-075-100.svg' }, ]; // ─── Registration Status Labels ─────────────────────────────────────────────── const REG_STATUSES = new Map([ [0, _('No Registration')], [2, _('Searching')], [8, _('Searching')], [3, _('Denied')], [4, _('Unknown')], [5, _('Roaming')], [7, _('Roaming')], [10, _('Roaming')], ]); // ─── Progress bar configuration ─────────────────────────────────────────────── // calc(vn, mn): vn = clamped signal value, mn = min boundary // All formulas normalise the value to 0–100% range for the progress bar. const PROGRESS_CONFIG = { rssi: { selector: '#rssi', min: -110, max: -50, // Linear map from [-110..-50] → [0..100] calc: (vn, mn) => Math.floor(100 * (1 - (-50 - vn) / (-50 - mn))), }, rsrp: { selector: '#rsrp', min: -140, max: -50, // Slightly expanded scale (×1.2) to better use bar width calc: (vn, mn) => Math.floor(120 * (1 - (-50 - vn) / (-70 - mn))), }, sinr: { selector: '#sinr', min: -20, max: 30, // Linear map from [-20..30] → [0..100] calc: (vn, mn) => Math.floor(100 - (100 * (1 - ((mn - vn) / (mn - 30))))), }, rsrq: { selector: '#rsrq', min: -20, max: 0, // Proportional map from [-20..0] → [0..100] calc: (vn, mn) => Math.floor(115 - (100 / mn) * vn), }, ecio: { selector: '#sinr', min: -24, max: 0, // Same shape as rsrq but for EC/IO range [-24..0] calc: (vn, mn) => Math.floor(100 - (100 / mn) * vn), }, }; // ─── LTE EARFCN → band/frequency table ─────────────────────────────────────── const LTE_BANDS = [ { min: 0, max: 599, frdl: 2110, frul: 1920, offset: 0, band: '1' }, { min: 600, max: 1199, frdl: 1930, frul: 1850, offset: 600, band: '2' }, { min: 1200, max: 1949, frdl: 1805, frul: 1710, offset: 1200, band: '3' }, { min: 1950, max: 2399, frdl: 2110, frul: 1710, offset: 1950, band: '4' }, { min: 2400, max: 2469, frdl: 869, frul: 824, offset: 2400, band: '5' }, { min: 2750, max: 3449, frdl: 2620, frul: 2500, offset: 2750, band: '7' }, { min: 3450, max: 3799, frdl: 925, frul: 880, offset: 3450, band: '8' }, { min: 6150, max: 6449, frdl: 791, frul: 832, offset: 6150, band: '20' }, { min: 9210, max: 9659, frdl: 758, frul: 703, offset: 9210, band: '28' }, { min: 9870, max: 9919, frdl: 452.5, frul: 462.5, offset: 9870, band: '31' }, { min: 37750, max: 38249, frdl: 2570, frul: 2570, offset: 37750, band: '38' }, { min: 38650, max: 39649, frdl: 2300, frul: 2300, offset: 38650, band: '40' }, { min: 39650, max: 41589, frdl: 2496, frul: 2496, offset: 39650, band: '41' }, ]; // ─── Non-LTE (UMTS/GSM) ARFCN → band/frequency table ───────────────────────── const NON_LTE_BANDS = [ { condition: rfcn => rfcn >= 10562 && rfcn <= 10838, calc: rfcn => ({ offset: 950, dlfreq: rfcn / 5, ulfreq: (rfcn - 950) / 5, band: 'IMT2100' }), }, { condition: rfcn => rfcn >= 2937 && rfcn <= 3088, calc: rfcn => ({ frul: 925, ulfreq: 340 + (rfcn / 5), dlfreq: (340 + (rfcn / 5)) - 45, band: 'UMTS900' }), }, { condition: rfcn => rfcn >= 955 && rfcn <= 1023, calc: rfcn => ({ frul: 890, ulfreq: 890 + ((rfcn - 1024) / 5), dlfreq: (890 + ((rfcn - 1024) / 5)) + 45, band: 'DSC900' }), }, { condition: rfcn => rfcn >= 512 && rfcn <= 885, calc: rfcn => ({ frul: 1710, ulfreq: 1710 + ((rfcn - 512) / 5), dlfreq: (1710 + ((rfcn - 512) / 5)) + 95, band: 'DCS1800' }), }, { condition: rfcn => rfcn >= 1 && rfcn <= 124, calc: rfcn => ({ frul: 890, ulfreq: 890 + (rfcn / 5), dlfreq: (890 + (rfcn / 5)) + 45, band: 'GSM900' }), }, ]; const UMTS_MODES = /(HS|3G|UMTS|WCDMA)/i; // ─── Helpers ────────────────────────────────────────────────────────────────── /** * Safe getElementById wrapper. * @param {string} id * @returns {HTMLElement|null} */ function getEl(id) { return document.getElementById(id); } /** * Show or hide the grandparent row of a signal element. * @param {HTMLElement} el * @param {boolean} show */ function setRowVisible(el, show) { const row = el && el.parentElement && el.parentElement.parentElement; if (row) row.style.display = show ? '' : 'none'; } /** * Update a cbi-progressbar element. * @param {string} type - Key in PROGRESS_CONFIG * @param {string} value - Raw signal value string (e.g. "-85 dBm") * @param {number} max - Boundary value (passed as second calc argument) * @param {number} idx - Modem index suffix for the element id */ function updateProgressBar(type, value, max, idx) { const config = PROGRESS_CONFIG[type]; if (!config) return; const pg = document.querySelector(`${config.selector}${idx}`); if (!pg) return; const vn = Math.max(config.min, Math.min(config.max, parseInt(value) || 0)); const mn = parseInt(max) || 100; const pc = Math.min(100, Math.max(0, config.calc(vn, mn))); pg.firstElementChild.style.width = `${pc}%`; pg.firstElementChild.style.animationDirection = 'reverse'; pg.setAttribute('title', String(value)); } /** * Format distance string, returns empty string when unavailable. * @param {string|number} dist * @returns {string} */ function formatDistance(dist) { if (!dist || dist === '--' || dist === '' || dist === '0.00') return ''; return ' ~' + dist + ' km'; } /** * Build the operator status badge HTML safely using LuCI E() helper. * @param {Object} modem - Single modem entry from JSON * @param {string} icon - URL to signal icon * @param {string} reg - Human-readable registration status * @returns {string} - Safe innerHTML string via dom.content */ function formatModemStatus(modem, icon, reg) { const rg = parseInt(modem.reg) || 0; const p = modem.csq_per || 0; const cops = modem.cops || '--'; const color = modem.csq_col || '#000000'; const distanceText = formatDistance(modem.distance); const iconEl = icon ? E('img', { 'class': 'modem-signal-icon', 'src': icon }) : null; const boldEl = E('b', { 'style': `color:${color}` }, [`${p}%`]); const children = iconEl ? [cops + ' ', iconEl, ' ', boldEl, distanceText] : [cops + ' ', boldEl, distanceText]; if (REGISTERED_STATUSES.includes(rg)) { return E('span', { 'class': 'ifacebadge' }, children).outerHTML; } else if (ROAMING_STATUSES.includes(rg)) { return E('span', { 'class': 'ifacebadge' }, [`${cops} (${reg}) `, ...(iconEl ? [iconEl, ' '] : []), boldEl, distanceText]).outerHTML; } else { return E('span', { 'class': 'ifacebadge' }, [reg || '--']).outerHTML; } } /** * Derive signal icon URL from signal percentage. * @param {number} pct * @returns {string} */ function resolveSignalIcon(pct) { const { icn } = SIGNAL_ICONS.find(({ max }) => pct <= max) || SIGNAL_ICONS[SIGNAL_ICONS.length - 1]; return L.resource(`view/modem/icons/${icn}`); } /** * Calculate DL/UL frequencies and band name for LTE mode. * @param {number} rfcn * @returns {{ dlfreq: number, ulfreq: number, band: string, frdl: number, frul: number, offset: number }} */ function calcLteBand(rfcn) { const b = LTE_BANDS.find(b => rfcn >= b.min && rfcn <= b.max); if (!b) return { frdl: 0, frul: 0, offset: 0, band: String(rfcn), dlfreq: 0, ulfreq: 0 }; const dlfreq = b.frdl + (rfcn - b.offset) / 10; const ulfreq = b.frul + (rfcn - b.offset) / 10; return { ...b, dlfreq, ulfreq }; } /** * Calculate DL/UL frequencies and band name for non-LTE modes. * @param {number} rfcn * @returns {{ dlfreq: number, ulfreq: number, band: string }} */ function calcNonLteBand(rfcn) { const match = NON_LTE_BANDS.find(b => b.condition(rfcn)); return match ? match.calc(rfcn) : { ulfreq: 0, dlfreq: 0, band: String(rfcn) }; } /** * Build a LAC/CID/eNB/Cell/PCI label and value string. * @param {Object} modem - Single modem entry * @returns {{ namecid: string, lactac: string }} */ function buildCellId(modem) { const { enbid, cell, pci, lac, cid } = modem; const parts = [lac, cid]; let namecid = 'LAC/CID'; if (enbid) { parts.push(enbid); namecid += '/eNB ID'; if (cell) { parts.push(cell); namecid += '/Cell'; if (pci) { parts.push(pci); namecid += '/PCI'; } } } const lactac = parts.join(' / '); return { namecid, lactac }; } /** * Derive mode-specific labels and carrier aggregation info. * @param {Object} modem - Single modem entry * @param {string} netmode * @param {string} band * @param {number} bw * @returns {{ carrier, bcc, bca, bwDisplay, namech, namesnr, namecid, lactac, namebnd }} */ function buildModeInfo(modem, netmode, band, bw) { let carrier = ''; let bcc, bca, bwDisplay, namech, namesnr, namecid, lactac; if (netmode === 'LTE') { const calte = modem.lteca; namech = 'EARFCN'; namesnr = 'SINR'; if (calte > 0) { carrier = '+'; bwDisplay = modem.bwca; bcc = ` B${band}${modem.scc}`; bca = bwDisplay ? ` / ${bwDisplay} MHz` : ''; } else { bwDisplay = bw; bcc = ` B${band}`; bca = bw ? ` / ${bw} MHz` : ''; } const cellInfo = buildCellId(modem); namecid = cellInfo.namecid; lactac = cellInfo.lactac; } else if (UMTS_MODES.test(netmode)) { namech = 'UARFCN'; namesnr = 'ECIO'; namecid = 'LAC/CID'; lactac = `${modem.lac} / ${modem.cid}`; bcc = ` ${band}`; } else { namech = 'ARFCN'; namesnr = 'SINR/ECIO'; namecid = 'LAC/CID'; lactac = `${modem.lac} / ${modem.cid}`; bcc = ` ${band}`; } const namebnd = bw ? _('Network/Band/Bandwidth') : _('Network/Band'); return { carrier, bcc, bca: bca || '', bwDisplay, namech, namesnr, namecid, lactac, namebnd }; } /** * Set textContent of element by id (if it exists). * @param {string} id * @param {string} text */ function setText(id, text) { const el = getEl(id); if (el) el.textContent = text; } /** * Set innerHTML of element by id (if it exists). * @param {string} id * @param {string} html */ function setHtml(id, html) { const el = getEl(id); if (el) el.innerHTML = html; } /** * Update a signal progress bar, hiding its parent row if value is missing. * @param {string} elId - Element id (without modem index) * @param {number} idx - Modem index * @param {*} rawVal - Raw value from modem JSON * @param {string} unit - Unit suffix, e.g. ' dBm' * @param {string} type - Key in PROGRESS_CONFIG * @param {number} boundary */ function updateSignalBar(elId, idx, rawVal, unit, type, boundary) { const el = getEl(elId + idx); if (!el) return; const missing = !rawVal || rawVal === '--' || rawVal === ''; setRowVisible(el, !missing); if (!missing) { updateProgressBar(type, rawVal + unit, boundary, idx); } } // ─── Main view ──────────────────────────────────────────────────────────────── return view.extend({ load: function() { return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}'); }, polldata: poll.add(function() { return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}') .then(function(res) { let json; try { json = JSON.parse(res); } catch (e) { console.error('modeminfo: JSON parse error', e); return; } if (!json || !Array.isArray(json.modem)) return; for (let i = 0; i < json.modem.length; i++) { const modem = json.modem[i]; const netmode = modem.mode || ''; const rfcn = modem.arfcn || 0; // ── Band / Frequency ──────────────────────────────────── let dlfreq, ulfreq, band, bw; if (netmode === 'LTE') { ({ dlfreq, ulfreq, band } = calcLteBand(rfcn)); const bandwidths = [1.4, 3, 5, 10, 15, 20]; bw = bandwidths[modem.bwdl] || ''; } else { ({ dlfreq, ulfreq, band } = calcNonLteBand(rfcn)); bw = ''; } const arfcnStr = `${rfcn} (${dlfreq} / ${ulfreq} MHz)`; // ── Registration ──────────────────────────────────────── const rg = Number(modem.reg); const reg = REG_STATUSES.get(rg) || _('No Data'); // ── Signal icon ───────────────────────────────────────── const icon = resolveSignalIcon(modem.csq_per || 0); // ── Mode-specific labels ──────────────────────────────── const { carrier, bcc, bca, namech, namesnr, namecid, lactac, namebnd } = buildModeInfo(modem, netmode, band, bw); // ── DOM updates ───────────────────────────────────────── setHtml('status' + i, formatModemStatus(modem, icon, reg)); const modeEl = getEl('mode' + i); if (modeEl) { // FIX: was `signal = 0` (assignment), must be `=== 0` if (modem.signal === 0 || modem.signal === '' || !netmode) { modeEl.textContent = '--'; } else { modeEl.textContent = `${netmode}${carrier} /${bcc}${bca}`; } } setText('namebnd' + i, namebnd); setText('chname' + i, namech); setText('namecid' + i, namecid); setText('snrname' + i, namesnr); setHtml('arfcn' + i, arfcnStr); setHtml('lac' + i, lactac); // ── Progress bars ─────────────────────────────────────── // RSSI always visible if element exists if (getEl('rssi' + i)) { if (!modem.rssi || modem.rssi === '') { setText('rssi' + i, '--'); } else { updateProgressBar('rssi', modem.rssi + ' dBm', -110, i); } } // SINR / ECIO (shared element, hidden when unavailable) updateSignalBar( 'sinr', i, modem.sinr, ' dB', netmode === 'LTE' ? 'sinr' : 'ecio', netmode === 'LTE' ? -20 : -24 ); // RSRP (hidden when unavailable) updateSignalBar('rsrp', i, modem.rsrp, ' dBm', 'rsrp', -140); // RSRQ (hidden when unavailable) updateSignalBar('rsrq', i, modem.rsrq, ' dB', 'rsrq', -20); } }); }), render: function(data) { let json; try { json = JSON.parse(data); } catch (e) { json = { modem: [] }; } const m = new form.Map('modeminfo', _('Modeminfo: Network'), _('Cellular network')); const s = m.section(form.TypedSection, 'general', null); s.anonymous = true; for (let i = 0; i < json.modem.length; i++) { const idx = i + 1; const statusId = 'status' + i; const modeId = 'mode' + i; const namebndId= 'namebnd'+ i; const chnameId = 'chname' + i; const namecidId= 'namecid'+ i; const arfcnId = 'arfcn' + i; const lacId = 'lac' + i; const rssiId = 'rssi' + i; const sinrId = 'sinr' + i; const snrnameId= 'snrname'+ i; const rsrpId = 'rsrp' + i; const rsrqId = 'rsrq' + i; let o; if (json.modem.length > 1) { s.tab('modem' + i, _('Modem') + ' ' + idx); o = s.taboption('modem' + i, form.HiddenValue, 'generic'); } else { o = s.option(form.HiddenValue, 'generic'); } o.render = L.bind(function() { return E('div', {}, [ E('h3', { 'class': 'data-tab' }), E('div', { 'class': 'cbi-section' }, [ E('table', { 'class': 'table' }, [ E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('Operator')]), E('td', { 'class': 'td left', 'id': statusId }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%', 'id': namebndId }, [_('Network/Band')]), E('td', { 'class': 'td left', 'id': modeId }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%', 'id': chnameId }, [_('E/U/ARFCN')]), E('td', { 'class': 'td left', 'id': arfcnId }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%', 'id': namecidId }, [_('LAC/CID')]), E('td', { 'class': 'td left', 'id': lacId }, ['--']), ]), E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('RSSI')]), E('td', { 'class': 'td left' }, E('div', { 'id': rssiId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) ), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%', 'id': snrnameId }, [_('SINR/EcIO')]), E('td', { 'class': 'td left' }, E('div', { 'id': sinrId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) ), ]), E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('RSRP')]), E('td', { 'class': 'td left' }, E('div', { 'id': rsrpId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) ), ]), E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ E('td', { 'class': 'td left', 'width': '50%' }, [_('RSRQ')]), E('td', { 'class': 'td left' }, E('div', { 'id': rsrqId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) ), ]), ]) ]) ]); }, this.polldata); o.anonymous = true; o.rmempty = true; } return m.render(); }, handleSaveApply: null, handleSave: null, handleReset: null, }); ================================================ FILE: luci/applications/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/setup.js ================================================ 'use strict'; 'require form'; 'require rpc'; 'require fs'; 'require view'; 'require uci'; 'require ui'; 'require tools.widgets as widgets' /* Written by Konstantine Shevlakov at 2023 - 2026 Licensed to the GNU General Public License v3.0. */ var callSerialPort = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ], expect: { entries: [] }, filter: function(list, params) { var rv = []; for (var i = 0; i < list.length; i++) if (list[i].name.match(/^ttyACM/) || list[i].name.match(/^ttyUSB/) || list[i].name.match(/^wwan\d+at\d+/)) rv.push(params.path + list[i].name); return rv.sort((a, b) => a.name > b.name); } }); var callQMIPort = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ], expect: { entries: [] }, filter: function(list, params) { var rv = []; for (var i = 0; i < list.length; i++) if (list[i].name.match(/^cdc-wdm/)) rv.push(params.path + list[i].name); return rv.sort(); } }); var callCheckQmiinfo = rpc.declare({ object: 'file', method: 'stat', params: [ 'path' ], expect: { '': {} } }); var maindesc = _('Modeminfo: Configuration'); var mdesc = _('Configuration panel of Modeminfo.'); var qfdesc = _('Get modem data via qmi. Require install qminfo.'); var sdesc = _('Select serial port.'); var qdesc = _('Select qmi port.'); var lacdec = _('Show LAC and CID in decimal.'); var mmdesc = _('Get device hardware name via mmcli utility if aviable.'); var qmidesc = _('Set qmi mode.'); var idesc = _('Show short info.
Overview: show info on main page \"Cellular Network\" section
MenuBar: show info on menubar all pages
NOTICE: Don\'t add too many modems in menubar, it may break the theme display.'); var portplace = _('Please select a port'); return view.extend({ load: function() { return callCheckQmiinfo('/usr/bin/qminfo') .then(function(stat) { return (stat && stat.size != null); }) .catch(function() { return false; }); }, render: function(qmiinfoInstalled){ var qfdesc = qmiinfoInstalled ? _('Get modem data via qmi.') : _('Please install qminfo package.'); var m, s, o; m = new form.Map('modeminfo', maindesc, mdesc); s = m.section(form.TypedSection, 'general', _('General option'), null); s.anonymous = true; o = s.option(form.ListValue, 'index', _('Short info'), idesc); o.widget="radio"; o.value(0,_('none')); o.value(1,_('overview')); o.value(2,_('menubar')); o.default = '0'; o.rmempty = true; o = s.option(form.Flag, 'decimail', _('Show decimal'), lacdec); o.rmempty = true; o = s.option(form.ListValue, 'delay', _('Interval'), _('Poll interval data')); o.value('', _('none')); o.value('1', '1 '+_('sec')); o.value('2', '2 '+_('sec')); o.value('5', '5 '+_('sec')); o.value('10', '10 '+_('sec')); o.value('30', '30 '+_('sec')); s = m.section(form.TypedSection, 'modeminfo', _('Devices setup'), null); s.anonymous = true; s.addremove = true; o = s.option(form.Flag, 'qmi_mode', _('Use QMI'), qfdesc); o.rmempty = true; if (!qmiinfoInstalled) { o.readonly = true; o.write = function() {}; o.cfgvalue = function() { return '0'; }; } o = s.option(form.ListValue, 'device', _('Data port'), sdesc); o.load = function(section_id) { return callSerialPort('/dev/').then(L.bind(function(devices) { this.keylist = []; this.vallist = []; devices.reverse().forEach(device => this.value(device)); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o.placeholder = portplace; o.rmempty = true; o.depends('qmi_mode', '0'); o = s.option(form.ListValue, 'device_qmi', _('Data port'), qdesc); o.load = function(section_id) { return callQMIPort('/dev/').then(L.bind(function(devices) { this.keylist = []; this.vallist = []; for (var i = 0; i < devices.length; i++) this.value(devices[i]); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o.placeholder = portplace; o.rmempty = true; o.depends('qmi_mode', '1'); o = s.option(form.Flag, 'mmcli_name', _('Name via mmcli'), mmdesc); o.rmempty = true; o.depends('qmi_mode', '0'); o = s.option(form.ListValue, 'qmi_trap', _('QMI mode'), qmidesc); o.value('',_('Auto')); o.value('qmi',_('Direct QMI')); o.value('mbim',_('QMI over MBIM')); o.rmempty = true; o.depends('qmi_mode', '1'); return m.render(); } }); ================================================ FILE: luci/applications/luci-app-modeminfo/htdocs/luci-static/resources/view/status/include/26_modeminfo.js ================================================ 'use strict'; 'require baseclass'; 'require form'; 'require fs'; 'require ui'; 'require uci'; /* Written by Konstantine Shevlakov at 2023 Licensed to the GNU General Public License v3.0. Special Thanks to Vladislav Kadulin aka @Kodo-kakaku https://github.com/Kodo-kakaku/ to fix update overview page */ return baseclass.extend({ title: _('Cellular network'), load: async function() { await uci.load('modeminfo'); var index = uci.sections('modeminfo', 'general'); if (!index || !index[0] || index[0].index !== "1") { return null; }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout: modeminfo execution took too long')); }, 30000); // 30 sec timeout fs.exec_direct('/usr/bin/modeminfo') .then(result => { clearTimeout(timeout); resolve(result); }) .catch(error => { clearTimeout(timeout); reject(error); }); }).catch(error => { console.error('Error loading modeminfo:', error); return ''; }); }, render: function(data){ if (!data || data.trim() === '') { var index = uci.sections('modeminfo', 'general'); if (!index || !index[0] || index[0].index !== "1") { return null; } else { return E('div', { 'class': 'cbi-section', 'style': 'color: #999; text-align: center;' }, _('Wait devices information')); } } try { var json = JSON.parse(data); var index = uci.sections('modeminfo', 'general'); let modemTBL = E('div', { 'class': 'cbi-section' }); if (json.modem && index[0].index == "1"){ for (var i = 0; i < json.modem.length; i++) { var signal = document.createElement('div'); var icn; var signalIcons = [ { max: -1, icn: 'signal-000-000.svg' }, { max: 0, icn: 'signal-000-000.svg' }, { max: 10, icn: 'signal-000-000.svg' }, { max: 25, icn: 'signal-000-025.svg' }, { max: 50, icn: 'signal-025-050.svg' }, { max: 75, icn: 'signal-050-075.svg' }, { max: Infinity, icn: 'signal-075-100.svg' } ]; var p = json.modem[i].csq_per || 0; var { icn } = signalIcons.find(({ max }) => p <= max); var icon = L.resource(`view/modem/icons/${icn}`); var per = p+'%'; var ca; if (json.modem[i].lteca > 0) { ca = "+"; } else { ca = ""; } signal.innerHTML = String.format( '' + json.modem[i].cops + " " + ''+per.fontcolor(json.modem[i].csq_col) + " " + json.modem[i].mode+ca + '', icon, p ); modemTBL.append( E('table', { 'class': 'table' }, [ E('tr', { 'class': 'tr' }, [ E('td', { 'class': 'td left', 'width': '33%' }, [ json.modem[i].device ]), E('td', { 'class': 'td left', 'id': 'device' }, [ signal ]) ]) ]) ); }; return modemTBL; }; } catch (error) { console.error('Error parsing modeminfo data:', error); return E('div', { 'class': 'cbi-section', 'style': 'color: #999; text-align: center;' }, _('Error loading information')); } }, }); ================================================ FILE: luci/applications/luci-app-modeminfo/po/ru/modeminfo.po ================================================ "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlyakov \n" msgid "Modem" msgstr "Модем" msgid "Modeminfo" msgstr "Модеминфо" msgid "Network" msgstr "Сеть" msgid "Hardware" msgstr "Оборудование" msgid "Setup" msgstr "Настройка" msgid "Modeminfo: Network" msgstr "Модеминфо: Сеть" msgid "No Registration" msgstr "Нет регистрации" msgid "Registered" msgstr "Зарегистрирован" msgid "Searching" msgstr "Поиск сети" msgid "Denied" msgstr "Запрет регистрации" msgid "Unknown" msgstr "Неизвестно" msgid "Roaming" msgstr "Гостевая сеть" msgid "No Data" msgstr "Нет данных" msgid "Network/Band/Bandwidth" msgstr "Сеть/Диапазон/Полоса" msgid "Network/Band" msgstr "Сеть/Диапазон" msgid "Cellular network" msgstr "Сотовая сеть" msgid "Operator" msgstr "Оператор" msgid "Status" msgstr "Состояние" msgid "Modeminfo: Hardware" msgstr "Модеминфо: Устройство" msgid "Hardware and sim-card info." msgstr "Информация о модеме и сим-карте" msgid "Device" msgstr "Устройство" msgid "Firmware" msgstr "Встроенное ПО" msgid "Chiptemp" msgstr "Температура" msgid "In automatic mode detect first answered DATA port." msgstr "В автоматическом режиме находит первый отвечающий порт данных." msgid "Please select a port" msgstr "Пожалуйста выберите порт" msgid "Modeminfo: Configuration" msgstr "Модеминфо: Конфигурация" msgid "Configuration panel of Modeminfo." msgstr "Страница конфигурации Modeminfo." msgid "Use QMI" msgstr "Использовать QMI" msgid "Get modem data via qmi. Require install qminfo." msgstr "Получать данные модема через qmi. Требуется установленный qminfo." msgid "Get modem data via qmi." msgstr "Получать данные модема через qmi." msgid "Please install qminfo package." msgstr "Установите пакет qminfo." msgid "Data port" msgstr "Порт данных" msgid "Select serial port." msgstr "Выберите последовательный порт." msgid "Select qmi port." msgstr "Выберите порт qmi." msgid "Show decimal" msgstr "Показать в десятичных" msgid "Show LAC and CID in decimal." msgstr "Показать LAC и CID соты в десятичных." msgid "Get device hardware name via mmcli utility if aviable." msgstr "Если доступно, использовать утилиту mmcli для имени модема." msgid "Name via mmcli" msgstr "Имя модема через mmcli" msgid "QMI mode" msgstr "Режим QMI" msgid "Set qmi mode." msgstr "Задействовать режим QMI." msgid "Auto" msgstr "Автоматически" msgid "Direct QMI" msgstr "Только QMI" msgid "QMI over MBIM" msgstr "QMI через MBIM" msgid "Short info" msgstr "Краткая информация" msgid "Show short info.
Overview: show info on main page \"Cellular Network\" section
MenuBar: show info on menubar all pages
NOTICE: Don\'t add too many modems in menubar, it may break the theme display." msgstr "Показвать краткую информацию
На главной: показывать главной странице раздел \"Сотовая сеть\"
Меню: показывать в строке меню на всех страницах
ВНИМАНИЕ: не добавляйте слишком много устройств в строку меню, это может сломать отображение." msgid "overview" msgstr "на главной" msgid "menubar" msgstr "в строке меню" msgid "sec" msgstr "сек" msgid "none" msgstr "нет" msgid "Interval" msgstr "Интервал" msgid "Poll interval data" msgstr "Интервал обновления данных" msgid "General option" msgstr "Общие настройки" msgid "Devices setup" msgstr "Настройки устройств" msgid "Wait devices information" msgstr "Ожидание ответа от устройств" msgid "Error loading information" msgstr "Получение данных завершилось ошибкой" ================================================ FILE: luci/applications/luci-app-modeminfo/po/zh_Hans/modeminfo.po ================================================ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" msgid "Modem" msgstr "移动网络" msgid "Modeminfo" msgstr "调制解调器信息" msgid "Network" msgstr "网络" msgid "China Mobile" msgstr "中国移动" msgid "China Unicom" msgstr "中国联通" msgid "China Telecom" msgstr "中国电信" msgid "Hardware" msgstr "硬件" msgid "Setup" msgstr "设置" msgid "Modeminfo: Network" msgstr "调制解调器信息: 网络" msgid "No Registration" msgstr "未注册" msgid "Registered" msgstr "已注册" msgid "Searching" msgstr "搜索中" msgid "Denied" msgstr "禁止注册" msgid "Unknown" msgstr "未知" msgid "Roaming" msgstr "漫游" msgid "No Data" msgstr "无数据" msgid "Network/Band/Bandwidth" msgstr "网络/频段/带宽" msgid "Network/Band" msgstr "网络/频段" msgid "Cellular network" msgstr "蜂窝网络" msgid "Operator" msgstr "运营商" msgid "Status" msgstr "状态" msgid "Modeminfo: Hardware" msgstr "调制解调器信息: 硬件" msgid "Hardware and sim card info." msgstr "硬件和sim卡信息。" msgid "Device" msgstr "设备" msgid "Firmware" msgstr "固件" msgid "Chiptemp" msgstr "芯片温度" msgid "In automatic mode detect first answered DATA port." msgstr "在自动模式下,检测第一个应答的DATA端口。" msgid "Please select a port" msgstr "请选择端口" msgid "Modeminfo: Configuration" msgstr "调制解调器信息:配置" msgid "Configuration panel of Modeminfo." msgstr "调制解调器信息配置面板。" msgid "Use QMI" msgstr "使用 QMI" msgid "Get modem data via qmicli (experimental). Require install qmi-utils." msgstr "通过qmicli获取调制解调器数据(实验)。需要安装qmi utils。" msgid "Data port" msgstr "数据端口" msgid "Select serial port." msgstr "选择串口。" msgid "Select qmi port." msgstr "选择qmi端口。" msgid "Show decimal" msgstr "十进制显示" msgid "Show LAC and CID in decimal." msgstr "以十进制显示LAC和CID。" msgid "Get device hardware name via mmcli utility if aviable." msgstr "如果可用,请通过mmcli 获取设备硬件名称。" msgid "Name via mmcli" msgstr "通过mmcli命名" msgid "QMI proxy" msgstr "QMI 代理" msgid "Enable qmi-proxy mode." msgstr "启用qmi代理模式。" msgid "Index page" msgstr "主页显示" msgid "Short info on Overview page" msgstr "在概览页面显示简单的信息。" ================================================ FILE: luci/applications/luci-app-modeminfo/root/usr/share/luci/menu.d/luci-app-modeminfo.json ================================================ { "admin/modem": { "title": "Modem", "order": 45, "action": { "type": "firstchild", "recurse": true } }, "admin/modem/main": { "title": "Modeminfo", "order": 10, "action": { "type": "alias", "path": "admin/modem/main/main" }, "depends": { "acl": [ "luci-app-modeminfo" ], "uci": { "modeminfo": true } } }, "admin/modem/main/main": { "title": "Network", "order": 51, "action": { "type": "view", "path": "modem/main" } }, "admin/modem/main/hw": { "title": "Hardware", "order": 52, "action": { "type": "view", "path": "modem/hw" } }, "admin/modem/main/config": { "title": "Setup", "order": 53, "action": { "type": "view", "path": "modem/setup" } } } ================================================ FILE: luci/applications/luci-app-modeminfo/root/usr/share/rpcd/acl.d/luci-app-modeminfo.json ================================================ { "luci-app-modeminfo": { "description": "Grant access to modeminfo configuration", "read": { "ubus": { "file": [ "read" ], "luci": [ "getConntrackHelpers" ] }, "file": { "/usr/bin/modeminfo": [ "exec" ], "/usr/share/modeminfo/scripts/rmtmp": [ "exec" ], "/sbin/uci": [ "exec" ] }, "uci": [ "modeminfo" ] }, "write": { "ubus": { "file": [ "write" ] }, "uci": [ "modeminfo" ] } } } ================================================ FILE: luci/applications/luci-app-mwan3-ledhelper/Makefile ================================================ include $(TOPDIR)/rules.mk LUCI_TITLE:=MWAN3 Ledhelper simple webUI LUCI_DEPENDS:=+luci-app-mwan3 +mwan3-ledhelper PKG_LICENSE:=GPLv3 PKG_VERSION:=0.0.1 PKG_RELEASE:=3 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-mwan3-ledhelper/htdocs/luci-static/resources/view/mwan3/network/led.js ================================================ 'use strict'; 'require form'; 'require fs'; 'require view'; 'require uci'; 'require rpc'; 'require ui'; 'require network'; 'require tools.widgets as widgets' var callLEDs = rpc.declare({ object: 'luci', method: 'getLEDs', expect: { '': {} } }); return view.extend({ load: function() { return Promise.all([ callLEDs(), L.resolveDefault(fs.list('/www' + L.resource('view/system/led-trigger')), []), // load config mwan3 fs.read('/etc/config/mwan3').catch(function(err) { return ''; }) ]).then(function(data) { var plugins = data[1]; var mwan3Config = data[2]; var tasks = []; // parce ifaces mwan3 config var mwan3Interfaces = []; if (mwan3Config) { var lines = mwan3Config.split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); var match = line.match(/^config interface '([^']+)'/); if (match) { mwan3Interfaces.push(match[1]); } } } for (var i = 0; i < plugins.length; i++) { var m = plugins[i].name.match(/^(.+)\.js$/); if (plugins[i].type != 'file' || m == null) continue; tasks.push(L.require('view.system.led-trigger.' + m[1]).then(L.bind(function(name){ return L.resolveDefault(L.require('view.system.led-trigger.' + name)).then(function(form) { return { name: name, form: form, }; }); }, this, m[1]))); } return Promise.all(tasks).then(function(plugins) { var value = {}; value[0] = data[0]; value[1] = plugins; value[2] = mwan3Interfaces; // add mwan3 iface return value; }); }); }, render: function(data) { var m, s, o, triggers = []; var leds = data[0]; var mwan3Interfaces = data[2]; // get ifaces from mwan3 config for (var k in leds) for (var i = 0; i < leds[k].triggers.length; i++) triggers[i] = leds[k].triggers[i]; m = new form.Map('mwan3_led', _('MWAN3 Ledhelper'), _('Flash led on link state.')); s = m.section(form.GridSection, 'led', null); s.anonymous = false; s.addremove = true; // filter NetworkSelect o = s.option(widgets.NetworkSelect, 'iface', _('Set interface')); o.exclude = s.section; o.nocreate = true; o.optional = true; // filter interfaces o.filter = function(section_id, value) { return mwan3Interfaces.indexOf(value) !== -1; }; o = s.option(form.ListValue, 'led_on', _('Select LED online')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.value('', _('Exclude')); o.default = ''; o.rmempty = true; o = s.option(form.ListValue, 'led_off', _('Select LED offline')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.value('', _('Exclude')); o.default = ''; o.rmempty = true; return m.render(); } }); ================================================ FILE: luci/applications/luci-app-mwan3-ledhelper/po/ru/mwan3-ledhelper.po ================================================ "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlakov \n" msgid "Flash led on link state." msgstr "Индикация светодиодов" msgid "Name" msgstr "Имя" msgid "Set interface" msgstr "Интерфейс" msgid "Exclude" msgstr "Не использовать" msgid "Select LED online" msgstr "LED \"в сети\"" msgid "Select LED offline" msgstr "LED \"вне сети\"" ================================================ FILE: luci/applications/luci-app-mwan3-ledhelper/root/usr/share/luci/menu.d/luci-app-mwan3-ledhelper.json ================================================ { "admin/network/mwan3/led": { "title": "LED", "order": 55, "action": { "type": "view", "path": "mwan3/network/led" } } } ================================================ FILE: luci/applications/luci-app-mwan3-ledhelper/root/usr/share/rpcd/acl.d/luci-app-mwan3-ledhelper.json ================================================ { "luci-app-mwan3-ledhelper": { "description": "Grant access to MWAN3 LED configuration", "read": { "file": { "/etc/init.d/mwan3": [ "exec" ], "/etc/config/mwan3": [ "read" ] }, "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "changes", "get" ] }, "uci": [ "mwan3_led" ] }, "write": { "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "add", "apply", "confirm", "delete", "order", "rename", "set" ] }, "uci": [ "mwan3_led" ] } } } ================================================ FILE: luci/applications/luci-app-ota/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Simple OTA Update for OpenWrt #LUCI_DEPENDS:= +luci-app-firewall PKG_LICENSE:=Apache-2.0 PKG_VERSION:=0.0.1 PKG_RELEASE:=3 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-ota/htdocs/luci-static/resources/view/system/ota.js ================================================ 'use strict'; 'require dom'; 'require fs'; 'require ui'; 'require view'; /* * OTA Update Application */ return view.extend({ load: function() { return Promise.resolve(); }, checkInitialState: function() { var self = this; this.verifyCheckResult().then(function(success) { if (success) { // If files already exist, disable Check button self.checkButton.disabled = true; // And show information about available update self.statusDiv.innerHTML = ''; self.statusDiv.appendChild(E('div', { 'class': 'alert-message success' }, [ E('h3', {}, _('Update Available')), E('pre', { 'style': 'white-space: pre-wrap; background: #f5f5f5; padding: 10px; border-radius: 3px; color: black;' }, self.changelog || '') ])); // Activate Upgrade button self.upgradeButton.disabled = false; } }).catch(function(err) { // No files, leave Check button active }); }, render: function() { var self = this; // Create control elements this.checkButton = E('button', { 'class': 'btn cbi-button cbi-button-positive important', 'click': ui.createHandlerFn(this, 'handleCheck') }, _('Check for Updates')); this.upgradeButton = E('button', { 'class': 'btn cbi-button cbi-button-negative important', 'disabled': true, 'click': ui.createHandlerFn(this, 'handleUpgrade') }, _('Upgrade')); this.statusDiv = E('div', { 'class': 'cbi-section' }); this.progressBar = E('div', { 'style': 'display: none; margin: 10px 0;' }); // Main layout var container = E('div', { 'class': 'cbi-map' }, [ E('h2', {}, _('OTA System Update')), E('div', { 'class': 'cbi-section' }, [ this.checkButton, ' ', this.upgradeButton ]), this.progressBar, this.statusDiv ]); // Check initial state when page loads this.checkInitialState(); return container; }, handleCheck: function() { var self = this; this.checkButton.disabled = true; this.statusDiv.innerHTML = ''; this.statusDiv.appendChild(E('div', { 'class': 'spinner' }, _('Checking for updates...'))); // Execute update check return fs.exec('/usr/share/ota.sh', ['check']) .then(function() { return self.verifyCheckResult(); }) .then(function(success) { if (success) { self.upgradeButton.disabled = false; self.statusDiv.innerHTML = ''; self.statusDiv.appendChild(E('div', { 'class': 'alert-message success' }, [ E('h3', {}, _('Update Available')), E('pre', { 'style': 'white-space: pre-wrap; background: #f5f5f5; padding: 10px; border-radius: 3px; color: black;' }, self.changelog || '') ])); } else { self.statusDiv.innerHTML = ''; self.statusDiv.appendChild(E('div', { 'class': 'alert-message warning' }, _('No updates available or check failed'))); } self.checkButton.disabled = false; }) .catch(function(err) { self.statusDiv.innerHTML = ''; self.statusDiv.appendChild(E('div', { 'class': 'alert-message error' }, _('Check failed: ') + (err.message || err))); self.checkButton.disabled = false; }); }, verifyCheckResult: function() { var self = this; return Promise.all([ fs.stat('/tmp/profiles.json').catch(function() { return null; }), fs.stat('/tmp/update.lock').catch(function() { return null; }), fs.read('/tmp/changelog.txt') .then(function(content) { self.changelog = content; return content && content.length > 0; }) .catch(function() { return false; }) ]).then(function(results) { // Check that all files exist and changelog is not empty return results[0] !== null && results[1] !== null && results[2] === true; }); }, handleUpgrade: function() { var self = this; this.upgradeButton.disabled = true; this.checkButton.disabled = true; this.progressBar.style.display = 'block'; // Initialize progress bar once this.progressBar.innerHTML = ''; // Create progress bar title this.progressTitle = E('div', { 'class': 'cbi-progressbar-title' }, _('Starting upgrade...')); this.progressBar.appendChild(this.progressTitle); // Create progress bar container this.currentProgressBar = E('div', { 'class': 'cbi-progressbar', 'style': 'margin: 10px 0;' }, E('div', { 'style': 'width: 0%' })); this.progressBar.appendChild(this.currentProgressBar); // First start progress simulation this.simulateProgressBar(); // Then start update process return fs.exec('/usr/share/ota.sh', ['upgrade']) .then(function(result) { // Wait for progress simulation to complete to 100% return new Promise(function(resolve) { var checkCompletion = function() { if (self.progressInterval) { setTimeout(checkCompletion, 100); } else { resolve(result); } }; checkCompletion(); }); }) .then(function(result) { // Show final message self.progressBar.style.display = 'none'; self.statusDiv.innerHTML = ''; self.statusDiv.appendChild(E('div', { 'class': 'alert-message warning' }, _('Download completed successfully! Start upgrade.
DO NOT POWER OFF THIS DEVICE!
System will be upgrade completed after reboot!'))); self.upgradeButton.disabled = false; self.checkButton.disabled = false; }) .catch(function(err) { // Stop progress simulation on error if (self.progressInterval) { clearInterval(self.progressInterval); self.progressInterval = null; } // Check if error is XHR timeout var errorMessage = err.message || err.toString(); if (errorMessage.includes('XHR request timed out')) { // For timeout show success message self.progressBar.style.display = 'none'; self.statusDiv.innerHTML = ''; self.statusDiv.appendChild(E('div', { 'class': 'alert-message warning' }, _('Download completed successfully! Start upgrade.
DO NOT POWER OFF THIS DEVICE!
System will be upgrade completed after reboot!'))); } else { // For other errors show error message self.progressTitle.textContent = _('Upgrade failed: ') + errorMessage; self.progressTitle.className = 'cbi-progressbar-title error'; } self.upgradeButton.disabled = false; self.checkButton.disabled = false; }); }, simulateProgressBar: function() { var self = this; if (this.progressInterval) { clearInterval(this.progressInterval); } let progress = 0; this.progressInterval = setInterval(function() { progress += Math.random() * 10 + 5; // Slowed down simulation if (progress >= 100) { progress = 100; clearInterval(self.progressInterval); self.progressInterval = null; self.updateProgressBar(progress, _('Download completed! Prepare upgrade...')); } else { self.updateProgressBar(progress, _('Downloading: ') + Math.round(progress) + '%'); } }, 800); // Increased interval }, updateProgressBar: function(percent, text) { if (!this.currentProgressBar || !this.progressTitle) return; // Update only text and progress, without recreating elements this.progressTitle.textContent = text; // Update progress bar as in modem example var percentValue = Math.min(100, Math.max(0, percent)); this.currentProgressBar.firstElementChild.style.width = percentValue + '%'; // Add animation as in example this.currentProgressBar.firstElementChild.style.animationDirection = "reverse"; }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-ota/po/ru/ota.po ================================================ "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlakov \n" msgid "OTA Update" msgstr "Обновление OTA" msgid "Update Available" msgstr "Доступно обновление" msgid "OTA System Update" msgstr "Обновление через OTA" msgid "Check for Updates" msgstr "Проверить" msgid "Upgrade" msgstr "Обновить" msgid "Checking for updates..." msgstr "Проверка обновлений..." msgid "No updates available or check failed" msgstr "Нет доступных обновлений" msgid "Check failed: " msgstr "Неудачно: " msgid "Download completed successfully! Start upgrade.
DO NOT POWER OFF THIS DEVICE!
System will be upgrade completed after reboot!" msgstr "Загрузка обновления завершена. Запущен процесс обновления.
НЕ ВЫКЛЮЧАЙТЕ УСТРОЙСТВО!
Система будет обновлена после перезагрузки!" msgid "Upgrade failed: " msgstr "Обновление прервано: " msgid "Download completed! Prepare upgrade..." msgstr "Загрузка выполнена. Подготовка..." ================================================ FILE: luci/applications/luci-app-ota/root/etc/config/ota ================================================ config ota option url 'https://openwrt.132lan.ru/releases_cell/latest' ================================================ FILE: luci/applications/luci-app-ota/root/usr/share/luci/menu.d/luci-app-ota.json ================================================ { "admin/system/ota": { "title": "OTA Update", "order": 70, "action": { "type": "view", "path": "system/ota" }, "depends": { "acl": [ "luci-app-ota" ] } } } ================================================ FILE: luci/applications/luci-app-ota/root/usr/share/ota.sh ================================================ #!/bin/sh # simple update firmware script for 132lan.ru site # by Konstantine Shevlakov (c) 2025 # for correct OTA upgrade remote dir must have next iherarchy: # www_root_dir/ # latest/ # changelog.txt - small note for update. # targets/ # SoC_family/ # SoC/ # openwrt-${DISTRIB_RELEASE}-${PREFIX1}-${PREFIX2}-${YEAR}${MONTH}-rev${REVNUMBER}*.bin # (e.g openwrt-24.10.2-4g-lte-202507-rev02-ramips-mt7621-beeline_smartbox-pro-squashfs-sysupgrade.bin) # profiles.json - list images generated by imagebuilder or buildroot # # In local firmware must be included local version in uci-defaults: # /rom/etc/uci_defaults/fw_rev next body # # FW_REV="${YEAR}${MONTH}-rev${REVNUMBER}" # e.g # FW_REV="202507-rev01" # # config URL Update server stored in # /etc/config/ota # config ota # option url 'https://example.com/latest' # # # example # # config ota # option url 'https://openwrt.132lan.ru/releases_cell/latest' . /lib/functions.sh load_config(){ config_get url $1 url } config_load ota config_foreach load_config URL_BASE=$url if [ "x${URL_BASE}" = "x" ]; then exit 0 fi # Get OpenWrt Release info [ -f /etc/openwrt_release ] && { . /etc/openwrt_release case ${DISTRIB_TARGET} in *x86*|*armsr*) echo "Current platform ${DISTRIB_TARGET} not supported." && exit 0 ;; esac } || { echo "Unknown OpenWrt release!" exit 0 } # Get board info BOARD=$(jsonfilter -s "$(cat /etc/board.json)" -e '@["model"]["id"]') # Remove update files remove_files(){ for f in firmware.bin changelog.txt; do [ -f /tmp/${f} ] && { rm -rf /tmp/${f} } done } case $1 in check) if [ ! -f /tmp/update.lock ]; then echo -e "Check updates from ${URL_BASE}\n" fi # Get aviable profiles ! [ -f /tmp/profiles.json ] && { wget ${URL_BASE}/targets/${DISTRIB_TARGET}/profiles.json -O /tmp/profiles.json > /dev/null 2>&1 } ! [ -f /tmp/profiles.json ] && { echo "Updates not available from this server." exit 0 } # Get changelog [ -f /tmp/profiles.json ] && { [ ! -f /tmp/changelog.txt ] && { wget ${URL_BASE}/changelog.txt -O /tmp/changelog.txt > /dev/null 2>&1 } || { break } } || { echo "No updates for this board: $BOARD" && remove_files && exit 0 } ;; esac # Get available boards BASE_BOARD=$(jsonfilter -s "$(cat /tmp/profiles.json)" -e '@["profiles"][*]["supported_devices"].*') # Get available board info board_stuff(){ IMAGES=$(jsonfilter -s "$(cat /tmp/profiles.json)" -e "@['profiles']['$FW_BOARD']['images'][*]" | grep sysupgrade) echo $IMAGES | jsonfilter \ -e FILE="$['name']" \ -e SHA256="$['sha256']" } #sha256check(){ #} # stuff for b in $BASE_BOARD; do if [ "${b}" = "${BOARD}" ]; then # modify board name for profiles.json FW_BOARD=$(echo $BOARD | sed -e 's/\,/_/') [ -f /tmp/profiles.json ] && { eval $(board_stuff) } || { echo "Failed! Abort update" remove_files } # Get remote revision firmware FW_REV_EXT=$(echo $FILE | awk -F [-] '{gsub("rev",""); gsub(/\./,"",$2); print $2$5$6}') # Get local revision firmware if [ -f /rom/etc/uci-defaults/fw_rev ]; then . /rom/etc/uci-defaults/fw_rev VER_LOCAL=$(echo $FW_REV | awk -F [-] '{gsub("rev",""); print $1$2}') DIGIT_RELEASE=$(echo ${DISTRIB_RELEASE} | awk '{gsub(/\./,""); print $0}') FW_VER_LOCAL=${DIGIT_RELEASE}${VER_LOCAL} else echo "Failed! Abort update" exit 0 fi # Firmware upgrade case $1 in upgrade) if [ -f /tmp/update.lock ]; then echo "Download firmware $FILE" echo "from $URL_BASE" wget $URL_BASE/targets/${DISTRIB_TARGET}/$FILE -O /tmp/firmware.bin > /dev/null 2>&1 case $? in 0) echo "Download complete." ;; *) echo "No updates for this board: $BOARD" && remove_files && exit 0 ;; esac # Get local SHA256 SHA256_DL=$(sha256sum /tmp/firmware.bin | awk '{print $1}') echo -n "Check sha256 sum: " # Compare remote and download file SHA256 if [ "$SHA256" = "$SHA256_DL" ]; then echo "OK" echo "Update process start!" echo "Device will be rebooted." echo "DO NOT TURN OFF DEVICE!" # Test firmware image before flashing sysupgrade -T /tmp/firmware.bin case $? in 0) echo "Flashing firmware" ;; *) echo "Failed! Abort update" && remove_files && exit 0 ;; esac rm -rf /tmp/update.lock /tmp/profiles.json /tmp/changelog.txt # Updrade firmware sleep 25 && sysupgrade /tmp/firmware.bin & else echo "Failed! Abort update." remove_files && rm -rf /tmp/update.lock /tmp/profiles.json fi fi ;; check) # Compare firmware versions if [ $FW_REV_EXT -gt $FW_VER_LOCAL ]; then echo "New firmware upgrade release!" echo -e "*** $FILE ***\n" [ -r /tmp/changelog.txt ] && { echo -e "RELEASE NOTES:\n" echo "$(cat /tmp/changelog.txt)" } echo "" echo "Please run script again for download and install update!" touch /tmp/update.lock else echo "Update not found!" fi ;; esac fi done #rm -f /tmp/profiles.json /tmp/update.lock /tmp/changelog.txt ================================================ FILE: luci/applications/luci-app-ota/root/usr/share/rpcd/acl.d/luci-app-ota.json ================================================ { "luci-app-ota": { "description": "OTA Update access", "read": { "file": { "/usr/share/ota.sh": ["exec"], "/tmp/changelog.txt": ["read"], "/tmp/profiles.json": ["read"], "/tmp/update.lock": ["read"] } }, "write": { "file": { "/usr/share/ota.sh": ["exec"] } } } } ================================================ FILE: luci/applications/luci-app-pingcontrol/Makefile ================================================ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-pingcontrol LUCI_DEPENDS:=+pingcontrol PKG_VERSION:=1 PKG_RELEASE:=3 LUCI_TITLE:=LuCI network interface control include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-pingcontrol/htdocs/luci-static/resources/view/pingcontrol/pingcontrol.js ================================================ 'use strict'; 'require form'; 'require view'; 'require uci'; 'require rpc'; 'require tools.widgets as widgets'; return view.extend({ handleEnableService: rpc.declare({ object: 'luci', method: 'setInitAction', params: [ 'pingcontrol', 'enable' ], expect: { result: false } }), render: function() { var m, s, o; m = new form.Map('pingcontrol', _('PingControl')); m.description = _('Server availability check'); s = m.section(form.GridSection, 'pingcontrol', _('Settings')); s.tab('general', _('General Settings')); s.tab('commands', _('Commands')); s.addremove = true; s.nodescriptions = true; o = s.taboption('general',form.Flag, 'enabled', _('Enabled')); o.rmempty = false; o.write = L.bind(function(section, value) { if (value == '1') { this.handleEnableService(); } return uci.set('pingcontrol', section, 'enabled', value); }, this); o = s.taboption('general',widgets.NetworkSelect, 'iface', _('Ping interface')); o.rmempty = false; o.textvalue = function(section_id) { return uci.get('pingcontrol', section_id, 'iface'); } o = s.taboption('general',form.DynamicList, 'testip', _('IP address or hostname of test servers')); o.datatype = 'or(hostname,ipaddr("nomask"))'; o = s.taboption('general',form.Flag, 'ping_silent', _('Silent mode'), _('Do not log success ping')); o = s.taboption('general',form.Value, 'check_period', _('Period of check, sec')); o.rmempty = false; o.datatype = 'and(uinteger,min(20))'; o.default = '60'; o = s.taboption('general',form.Value, 'sw_before_modres', _('Failed attempts before iface up/down'), _('0 - not used')); o.rmempty = false; o.datatype = 'and(uinteger,min(0),max(100))'; o.default = '3'; o = s.taboption('general',form.Value, 'sw_before_sysres', _('Failed attempts before reboot'), _('0 - not used')); o.rmempty = false; o.datatype = 'and(uinteger,min(0),max(100))'; o.default = '0'; o = s.taboption('commands',form.Value, 'ping_ok', _('Successful ping')); o.modalonly = true; o = s.taboption('commands',form.Value, 'ping_lost', _('Ping lost')); o.modalonly = true; o = s.taboption('commands',form.Value, 'ping_restored', _('Ping restored')); o.modalonly = true; o = s.taboption('commands',form.Value, 'before_iface_down', _('Before interface down')); o.modalonly = true; o = s.taboption('commands',form.Value, 'after_iface_up', _('After interface up')); o.modalonly = true; o = s.taboption('commands',form.Value, 'before_reboot', _('Before reboot')); o.modalonly = true; return m.render(); } }); ================================================ FILE: luci/applications/luci-app-pingcontrol/po/en/pingcontrol.po ================================================ msgid "Server availability check" msgstr "Server availability check" msgid "Period of check, sec" msgstr "Period of check, sec" msgid "Ping interface" msgstr "Ping interface" msgid "IP address or hostname of test servers" msgstr "IP address or hostname of test servers" msgid "Failed attempts before iface up/down" msgstr "Failed attempts before iface up/down" msgid "Failed attempts before reboot" msgstr "Failed attempts before reboot" msgid "Configuration name" msgstr "Configuration name" ================================================ FILE: luci/applications/luci-app-pingcontrol/po/ru/pingcontrol.po ================================================ msgid "Server availability check" msgstr "Проверка доступности сервера" msgid "Period of check, sec" msgstr "Период проверки, сек" msgid "Ping interface" msgstr "Интерфейс для ping-запросов" msgid "IP address or hostname of test servers" msgstr "IP-адрес или имя хоста тестовых серверов" msgid "Silent mode" msgstr "Тихий режим" msgid "Do not log success ping" msgstr "Не журналировать успешные попытки" msgid "Failed attempts before iface up/down" msgstr "Количество неудачных попыток до переподключения интерфейса" msgid "Failed attempts before reboot" msgstr "Количество неудачных попыток до перезагрузки устройства" msgid "Configuration name" msgstr "Имя конфигурации" msgid "Commands" msgstr "Команды" msgid "Successful ping" msgstr "Пинг успешен" msgid "Ping lost" msgstr "Пинг потерян" msgid "Ping restored" msgstr "Пинг восстановлен" msgid "Before interface down" msgstr "Перед отключением интерфейса" msgid "After interface up" msgstr "После включения интерфейса" msgid "Before reboot" msgstr "Перед перезагрузкой" ================================================ FILE: luci/applications/luci-app-pingcontrol/root/usr/share/luci/menu.d/luci-app-pingcontrol.json ================================================ { "admin/services/pingcontrol": { "title": "Pingcontrol", "action": { "type": "view", "path": "pingcontrol/pingcontrol" }, "depends": { "acl": [ "luci-app-pingcontrol" ], "uci": { "pingcontrol": true } } } } ================================================ FILE: luci/applications/luci-app-pingcontrol/root/usr/share/rpcd/acl.d/luci-app-pingcontrol.json ================================================ { "luci-app-pingcontrol": { "description": "Grant UCI access for luci-app-pingcontrol", "read": { "uci": [ "pingcontrol" ] }, "write": { "uci": [ "pingcontrol" ] } } } ================================================ FILE: luci/applications/luci-app-rtorrent/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: luci/applications/luci-app-rtorrent/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=rTorrent LuCI web interface LUCI_DEPENDS:=+rtorrent-rpc +luaexpat +luasocket +luasec +screen PKG_VERSION:=0.1.7 PKG_RELEASE:=1 PKG_LICENSE:=GPLv3 define Package//luci-app-rtorrent/conffiles /etc/config/rtorrent /etc/rtorrent.conf endef define Package/luci-app-rtorrent/postinst rm -rf /tmp/luci-indexcache /tmp/luci-modulecache endef define Package/luci-app-rtorrent/postrm rm -rf /tmp/luci-indexcache /tmp/luci-modulecache endef include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-rtorrent/README.md ================================================ # luci-app-rtorrent rTorrent client for OpenWrt's LuCI web interface ## Features - List all torrent downloads - Add new torrent by url/magnet uri/file - Stop/start/delete torrents - Mark torrents with tags - Set priority per file - Enable/disable and add trackers to torrent - Detailed peer listing - Completely LuCI based interface - OpenWrt device independent (written in lua) - Opkg package manager support - RSS feed downloader (automatically download torrents that match the specified criteria) - Support for [Transdroid](https://www.transdroid.org/) ([Transdrone](https://play.google.com/store/apps/details?id=org.transdroid.lite)) ## Screenshots [luci-app-rtorrent 0.1.3](https://github.com/wolandmaster/luci-app-rtorrent/wiki/Screenshots) ## Install instructions (for Openwrt 15.05.1 Chaos Calmer) ### Install rtorrent-rpc ``` opkg update opkg install rtorrent-rpc ``` (Note: If you going to install rtorrent-rpc to an own [opkg destination](https://wiki.openwrt.org/doc/techref/opkg#installation_destinations) then you have to install libopenssl to the root destination before) ### Create rTorrent config file #### Minimal _/root/.rtorrent.rc_ file: ``` directory = /path/to/downloads/ session = /path/to/session/ scgi_port = 127.0.0.1:5000 schedule = rss_downloader,300,300,"execute=/usr/lib/lua/rss_downloader.lua" ``` #### Sample _/root/.rtorrent.rc_ file: http://pissedoffadmins.com/os/linux/sample-rtorrent-rc-file.html #### Recommended kernel parameters to avoid low memory issues: ``` cat /etc/sysctl.conf ... # handle rtorrent related low memory issues vm.swappiness=95 vm.vfs_cache_pressure=200 vm.min_free_kbytes=4096 vm.overcommit_memory=2 vm.overcommit_ratio=60 ``` ### Create init.d script (optional) #### Install screen ``` opkg install screen ``` #### Create _/etc/init.d/rtorrent_ script ``` #!/bin/sh /etc/rc.common START=99 STOP=99 SCREEN=/usr/sbin/screen PROG=/usr/bin/rtorrent start() { sleep 3 $SCREEN -dm -t rtorrent nice -19 $PROG } stop() { killall rtorrent } ``` #### Start rtorrent ``` chmod +x /etc/init.d/rtorrent /etc/init.d/rtorrent enable /etc/init.d/rtorrent start ``` ### Install wget (the wget in busybox does not support https) ``` opkg install ca-certificates opkg install wget ln -sf $(which wget-ssl) /usr/bin/wget ``` ### Install luci-app-rtorrent ``` wget -nv https://github.com/wolandmaster/luci-app-rtorrent/releases/download/latest/e1a1ba8004c4220f -O /etc/opkg/keys/e1a1ba8004c4220f echo 'src/gz luci_app_rtorrent https://github.com/wolandmaster/luci-app-rtorrent/releases/download/latest' >> /etc/opkg.conf opkg update opkg install luci-app-rtorrent ``` ### Upgrade already installed version ``` opkg update opkg upgrade luci-app-rtorrent ``` ### References ================================================ FILE: luci/applications/luci-app-rtorrent/htdocs/cgi-bin/rtorrent ================================================ #!/usr/bin/lua local rtorrent = require "rtorrent" io.write(rtorrent.rpc(io.read("*all"))) io.flush() ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local nixio = require "nixio" local dm = require "luci.model.cbi.rtorrent.download" module("luci.controller.rtorrent", package.seeall) function index() entry({"admin", "rtorrent"}, firstchild(), translate("Torrent"), 45).dependent = false entry({"admin", "rtorrent", "main"}, form("rtorrent/main"), translate("Torrent List"), 10).leaf = true entry({"admin", "rtorrent", "add"}, form("rtorrent/add", {autoapply=true}), translate("Add Torrent"), 20) entry({"admin", "rtorrent", "rss"}, arcombine(cbi("rtorrent/rss"), cbi("rtorrent/rss-rule")), translate("RSS Downloader"), 30).leaf = true entry({"admin", "rtorrent", "admin"}, form("rtorrent/admin/rtorrent"), translate("Torrent Settings"), 40) entry({"admin", "rtorrent", "info"}, form("rtorrent/torrent/info"), nil).leaf = true entry({"admin", "rtorrent", "files"}, form("rtorrent/torrent/files"), nil).leaf = true entry({"admin", "rtorrent", "trackers"}, form("rtorrent/torrent/trackers"), nil).leaf = true entry({"admin", "rtorrent", "peers"}, form("rtorrent/torrent/peers"), nil).leaf = true entry({"admin", "rtorrent", "download"}, call("download"), nil).leaf = true entry({"admin", "rtorrent", "downloadall"}, call("downloadall"), nil).leaf = true entry({"admin", "rtorrent", "admin", "rtorrent"}, form("rtorrent/admin/rtorrent"), nil).leaf = true entry({"admin", "rtorrent", "admin", "rss"}, cbi("rtorrent/admin/rss"), nil).leaf = true end function download() dm.download_file(nixio.bin.b64decode(luci.dispatcher.context.requestpath[4])) end function downloadall() dm.download_all(nixio.bin.b64decode(luci.dispatcher.context.requestpath[4])) end ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local bencode = require "bencode" local nixio = require "nixio" local rtorrent = require "rtorrent" local xmlrpc = require "xmlrpc" local common = require "luci.model.cbi.rtorrent.common" require "luci.model.cbi.rtorrent.string" f = SimpleForm("rtorrent", translate("Add Torrent")) f.submit = "Add" local torrent uri = f:field(TextValue, "uri", translate("Torrent
or magnet URI")) uri.rows = 1 function uri.validate(self, value, section) if "magnet:" == string.sub(value:trim(), 1, 7) then torrent = bencode.encode({ ["magnet-uri"] = value:trim() }) else local ok, res = common.get(value) if not ok then return nil, "Not able to download torrent: " .. res end local tab, err = bencode.decode(res) if not tab then return nil, "Not able to parse torrent file: " .. err end torrent = res end return value end file = f:field(FileUpload, "file", translate("Upload torrent file")) file.root_directory = "/etc/luci-uploads" function file.validate(self, value, section) torrent = nixio.fs.readfile(value) self:remove(section) local tab, err = bencode.decode(torrent) if not tab then return nil, "Not able to parse torrent file: " .. err end return value end dir = f:field(Value, "dir", translate("Download directory")) dir.default = rtorrent.call("directory.default") dir.datatype = "directory" dir.rmempty = false tags = f:field(Value, "tags", translate("Tags")) local user = luci.dispatcher.context.authuser tags.default = "all" .. (user ~= "root" and " " .. user or "") tags.rmempty = false start = f:field(Flag, "start", translate("Start now")) start.default = "1" start.rmempty = false function f.handle(self, state, data) if state == FORM_VALID and torrent and #torrent > 0 then local params = {} table.insert(params, data.start == "1" and "load.raw_start" or "load.raw") table.insert(params, "") -- target table.insert(params, xmlrpc.newTypedValue((nixio.bin.b64encode(torrent)), "base64")) table.insert(params, "d.directory.set=\"" .. data.dir .. "\"") table.insert(params, "d.custom1.set=\"" .. data.tags .. "\"") if data.uri then table.insert(params, "d.custom3.set=" .. nixio.bin.b64encode(data.uri)) end rtorrent.call(unpack(params)) luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/add")) end return true end return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rss.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local common = require "luci.model.cbi.rtorrent.common" local nixio = require "nixio" m = Map("rtorrent", "Admin - RSS Downloader") s = m:section(TypedSection, "rss-feed") s.addremove = true s.anonymous = true s.sortable = true s.template = "cbi/tblsection" s.render = function(self, section, scope) luci.template.render("rtorrent/tabmenu", { self = { pages = common.get_admin_pages(), page = "RSS" }}) TypedSection.render(self, section, scope) end name = s:option(Value, "name", "Name") name.rmempty = false url = s:option(Value, "url", "RSS Feed URL") url.size = "65" url.rmempty = false enabled = s:option(Flag, "enabled", "Enabled") enabled.rmempty = false t = m:section(NamedSection, "logging", "rss", "Logging") feed_logging = t:option(Flag, "feed_logging", "Enable RSS feed logging") feed_logfile = t:option(Value, "feed_logfile", "RSS feed logfile") feed_logfile:depends("feed_logging", 1) function feed_logfile.validate(self, value, section) local parent_folder = nixio.fs.dirname(value) if parent_folder == "." or nixio.fs.stat(parent_folder, "type") ~= "dir" then return nil, "Wrong filename, please use absolute path!" end return value end return m ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local rtorrent = require "rtorrent" local common = require "luci.model.cbi.rtorrent.common" local config = rtorrent.batchcall({ "throttle.global_up.max_rate", "throttle.global_down.max_rate", "throttle.max_downloads.global", "throttle.max_uploads.global", "throttle.max_uploads", "throttle.min_peers.normal", "throttle.max_peers.normal", "throttle.min_peers.seed", "throttle.max_peers.seed" }) local function set_config(key, value) if tonumber(value) ~= config[key] then rtorrent.call(key .. ".set", "", value) luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/admin/rtorrent")) end end f = SimpleForm("rtorrent", translate("Admin - rTorrent")) speed = f:section(SimpleSection) speed.title = translate("Bandwidth limits") speed.render = function(self, ...) luci.template.render("rtorrent/tabmenu", { self = { pages = common.get_admin_pages(), page = "rTorrent" }}) SimpleSection.render(self, ...) end upload_rate = speed:option(Value, "upload_rate", translate("Upload limit (KiB/sec)"), translate("Global upload rate (0: unlimited)")) upload_rate.rmempty = false upload_rate.default = config["throttle.global_up.max_rate"] / 1024 upload_rate.datatype = "uinteger" function upload_rate.write(self, section, value) set_config("throttle.global_up.max_rate", value .. "k") end download_rate = speed:option(Value, "download_rate", translate("Download limit (KiB/sec)"), translate("Global downlaod rate (0: unlimited)")) download_rate.rmempty = false download_rate.default = config["throttle.global_down.max_rate"] / 1024 download_rate.datatype = "uinteger" function download_rate.write(self, section, value) set_config("throttle.global_down.max_rate", value .. "k") end global_limits = f:section(SimpleSection) global_limits.title = translate("Global limits") max_downloads_global = global_limits:option(Value, "max_downloads_global", translate("Download slots"), translate("Maximum number of simultaneous downloads")) max_downloads_global.rmempty = false max_downloads_global.default = config["throttle.max_downloads.global"] max_downloads_global.datatype = "uinteger" function max_downloads_global.write(self, section, value) set_config("throttle.max_downloads.global", value) end max_uploads_global = global_limits:option(Value, "max_uploads_global", translate("Upload slots"), translate("Maximum number of simultaneous uploads")) max_uploads_global.rmempty = false max_uploads_global.default = config["throttle.max_uploads.global"] max_uploads_global.datatype = "uinteger" function max_uploads_global.write(self, section, value) set_config("throttle.max_uploads.global", value) end torrent_limits = f:section(SimpleSection) torrent_limits.title = translate("Torrent limits") max_uploads = torrent_limits:option(Value, "max_uploads", translate("Maximum uploads"), translate("Maximum number of simultanious uploads per torrent")) max_uploads.rmempty = false max_uploads.default = config["throttle.max_uploads"] max_uploads.datatype = "uinteger" function max_uploads.write(self, section, value) set_config("throttle.max_uploads", value) end min_peers = torrent_limits:option(Value, "min_peers", translate("Minimum peers"), translate("Minimum number of peers to connect to per torrent")) min_peers.rmempty = false min_peers.default = config["throttle.min_peers.normal"] min_peers.datatype = "uinteger" function min_peers.write(self, section, value) set_config("throttle.min_peers.normal", value) end max_peers = torrent_limits:option(Value, "max_peers", translate("Maximum peers"), translate("Maximum number of peers to connect to per torrent")) max_peers.rmempty = false max_peers.default = config["throttle.max_peers.normal"] max_peers.datatype = "uinteger" function max_peers.write(self, section, value) set_config("throttle.max_peers.normal", value) end min_peers_seed = torrent_limits:option(Value, "min_peers_seed", translate("Minimum seeds"), translate("Minimum number of seeds for completed torrents (-1 = same as peers)")) min_peers_seed.rmempty = false min_peers_seed.default = config["throttle.min_peers.seed"] min_peers_seed.datatype = "integer" function min_peers_seed.write(self, section, value) set_config("throttle.min_peers.seed", value) end max_peers_seed = torrent_limits:option(Value, "max_peers_seed", translate("Maximum seeds"), translate("Maximum number of seeds for completed torrents (-1 = same as peers)")) max_peers_seed.rmempty = false max_peers_seed.default = config["throttle.max_peers.seed"] max_peers_seed.datatype = "integer" function max_peers_seed.write(self, section, value) set_config("throttle.max_peers.seed", value) end -- dir = f:field(DummyValue, "dummy", luci.dispatcher.context.authuser) return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/common.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local http = require "socket.http" local https = require "ssl.https" local ssl = require "ssl" local ltn12 = require "ltn12" local fs = require "nixio.fs" local dispatcher = require "luci.dispatcher" require "luci.model.cbi.rtorrent.string" local string, os, io, math, assert = string, os, io, math, assert local ipairs, table, unpack, tonumber = ipairs, table, unpack, tonumber local COOKIES_FILE = "/etc/cookies.txt" module "luci.model.cbi.rtorrent.common" function get_torrent_pages(hash) return { { name = "Info", link = dispatcher.build_url("admin/rtorrent/info/") .. hash }, { name = "File List", link = dispatcher.build_url("admin/rtorrent/files/") .. hash }, { name = "Tracker List", link = dispatcher.build_url("admin/rtorrent/trackers/") .. hash }, { name = "Peer List", link = dispatcher.build_url("admin/rtorrent/peers/") .. hash } } end function get_admin_pages() return { { name = "rTorrent", link = dispatcher.build_url("admin/rtorrent/admin/rtorrent") }, { name = "RSS", link = dispatcher.build_url("admin/rtorrent/admin/rss") } } end function human_size(bytes) local symbol = {[0]="B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} local exp = bytes > 0 and math.floor(math.log(bytes) / math.log(1024)) or 0 local value = bytes / math.pow(1024, exp) local acc = bytes > 0 and 2 - math.floor(math.log10(value)) or 2 if acc < 0 then acc = 0 end return string.format("%." .. acc .. "f " .. symbol[exp], value) end function human_time(sec) local t = os.date("*t", sec) if t["day"] > 25 then return "∞" elseif t["day"] > 1 then return string.format("%dd
%dh %dm", t["day"] - 1, t["hour"], t["min"]) elseif t["hour"] > 1 then return string.format("%dh
%dm %ds", t["hour"] - 1, t["min"], t["sec"]) elseif t["min"] > 0 then return string.format("%dm %ds", t["min"], t["sec"]) else return string.format("%ds", t["sec"]) end end function div(body, ...) local class = {} for _, c in ipairs({...}) do if c then table.insert(class, c) end end if #class > 0 then return "
%s
" % {table.concat(class, " "), body} else return body end end function wget(url) local file = assert(io.popen("/usr/bin/wget " .. "--quiet " .. "--user-agent=unknown " .. "--referer=http://www.google.com " .. "--load-cookies=" .. COOKIES_FILE .. " " .. "--output-document=- " .. url, "r")) local response = file:read("*all") file:close() return response end function get(url) local response = {} local proto = url:starts("https://") and https or http proto.TIMEOUT = 5 local body, code, headers, status = proto.request { method = "GET", headers = { ["Referer"] = "http://www.google.com", ["User-Agent"] = "unknown", ["Cookie"] = get_cookies(url) }, url = url, redirect = (proto.PORT == 80) and true or nil, sink = ltn12.sink.table(response) } if code == 301 then return get(headers["location"]) end if code == 200 then return true, table.concat(response) else local body = wget(url) if body:len() > 0 then return true, body else return false, status end end end function get_cookies(url) local cookies = {} for _, line in ipairs(fs.readfile(COOKIES_FILE):split("\n\r")) do if not line:match("^\s*#.*") then local domain, tailmatch, path, secure, expiration, name, value = unpack(line:split()) local url_match = (secure == "TRUE") and "^https://" or "^https?://" url_match = url_match .. (tailmatch == "TRUE" and ".*" or "") .. domain .. path if url:match(url_match) and tonumber(expiration) >= os.time() then table.insert(cookies, name .. "=" .. value) end end end return table.concat(cookies, "; ") end ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/download.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local nixio = require "nixio" local http = require "luci.http" local ltn12 = require "luci.ltn12" local sys = require "luci.sys" local common = require "luci.model.cbi.rtorrent.common" require "luci.model.cbi.rtorrent.string" local ipairs, string = ipairs, string local PROTECTED_PATH = {"/bin", "/dev", "/etc", "/lib", "/overlay", "/root", "/sbin", "/tmp", "/usr", "/var", "/www"} module("luci.model.cbi.rtorrent.download", package.seeall) function security_check(file) for _, path_prefix in ipairs(PROTECTED_PATH) do if file:starts(path_prefix) then http.write("

Access Denied

") return false end end return true end function download_file(file) file = nixio.fs.realpath(file) if security_check(file) then local f = nixio.open(file, "r") http.header('Content-Disposition', 'attachment; filename="%s"' % nixio.fs.basename(file)) http.header('Content-Length', nixio.fs.stat(file, "size")) http.prepare_content("application/octet-stream") repeat local buf = f:read(2^13) -- 8k http.write(buf) until (buf == "") f:close() end end function download_all(path) path = nixio.fs.realpath(path) if security_check(path) then if string.find(string.lower(http.getenv("HTTP_USER_AGENT")), "linux") then download_all_as_tar(path) else download_all_as_zip(path) end end end function download_all_as_zip(path) local reader = sys.ltn12_popen("zip -0 -j -r - \"%s\"" % path) http.header('Content-Disposition', 'attachment; filename="%s.zip"' % nixio.fs.basename(path)) http.prepare_content("application/zip") ltn12.pump.all(reader, http.write) end function download_all_as_tar(path) local reader = sys.ltn12_popen("tar -cf - -C \"%s\" ." % path) http.header('Content-Disposition', 'attachment; filename="%s.tar"' % nixio.fs.basename(path)) http.prepare_content("application/x-tar") ltn12.pump.all(reader, http.write) end ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. -- Custom fields: -- d.custom1: tags (space delimited) -- d.custom2: tracker favicon -- d.custom3: url of torrent file -- d.custom4: not used -- d.custom5: when "1": delete files from disk on erase local common = require "luci.model.cbi.rtorrent.common" local rtorrent = require "rtorrent" require "luci.model.cbi.rtorrent.string" local selected, format, total = {}, {}, {} local methods = { "hash", "name", "size_bytes", "bytes_done", "hashing", "state", "complete", "peers_accounted", "peers_complete", "down.rate", "up.rate", "ratio", "up.total", "timestamp.started", "timestamp.finished", "custom1", "custom2" } function status(d) if d["hashing"] > 0 then return "hash" elseif d["state"] == 0 then return "stop" elseif d["state"] > 0 then if d["complete"] == 0 then return "down" else return "seed" end else return "unknown" end end function eta(d) if d["bytes_done"] < d["size_bytes"] then if d["down_rate"] > 0 then return common.human_time((d["size_bytes"] - d["bytes_done"]) / d["down_rate"]) else return "∞" end else return "--" end end function favicon(d) if not d["custom2"] or d["custom2"] == "" then d["custom2"] = "/luci-static/resources/icons/unknown_tracker.png" for _, t in pairs(rtorrent.multicall("t.", d["hash"], 0, "url", "is_enabled")) do if t["is_enabled"] then local domain = t["url"]:match("[%w%.:/]*[%./](%w+%.%w+)") if domain then local icon = "http://" .. domain .. "/favicon.ico" if common.get(icon) then d["custom2"] = icon break end end end end rtorrent.call("d.custom2.set", d["hash"], d["custom2"]) end return d["custom2"] end function has_tag(tags, tag) for _, t in ipairs(tags) do if t.name:lower() == tag:lower() then return true end end return false end function get_tags(details) local l = {} local has_incomplete = false for _, d in ipairs(details) do for _, p in ipairs(d["custom1"]:split()) do if not has_tag(l, p) then table.insert(l, {name = p:ucfirst(), link = luci.dispatcher.build_url("admin/rtorrent/main/%s" % p)}) end end if d["complete"] == 0 then has_incomplete = true end end if has_incomplete then table.insert(l, {name = translate("Incomplete"), link = luci.dispatcher.build_url("admin/rtorrent/main/incomplete")}) end return l end function filter(details, page) local filtered = {} for _, d in ipairs(details) do if string.find(" " .. d["custom1"] .. " ", " " .. page .. " ") then table.insert(filtered, d) end if page == "incomplete" and d["complete"] == 0 then table.insert(filtered, d) end end return filtered end function format.icon(d, v) return "" end function format.name(d, v) total["name"] = (total["name"] or 0) + 1 local url = luci.dispatcher.build_url("admin/rtorrent/files/" .. d["hash"]) return "%s" % {url, v} end function format.size_bytes(d, v) total["size_bytes"] = (total["size_bytes"] or 0) + v return "
%s
" % {v, common.human_size(v)} end function format.done_percent(d, v) return string.format("%.1f%%", v) end function format.status(d, v) return common.div(v, v == "stop" and "red", v == "seed" and "blue", v == "down" and "green", v == "hash" and "green") end function format.down_rate(d, v) total["down_rate"] = (total["down_rate"] or 0) + v return string.format("%.2f", v / 1000) end function format.up_rate(d, v) total["up_rate"] = (total["up_rate"] or 0) + v return string.format("%.2f", v / 1000) end function format.ratio(d, v) return common.div(string.format("%.2f", v / 1000), v < 1000 and "red" or "green") -- "title: Total uploaded: " .. common.human_size(d["up_total"])) end function format.eta(d, v) local download_started = d["timestamp_started"] == 0 and translate("not yet started") or os.date("!%Y-%m-%d %H:%M:%S", d["timestamp_started"]) local download_finished = d["timestamp_finished"] == 0 and translate("not yet finished") or os.date("!%Y-%m-%d %H:%M:%S", d["timestamp_finished"]) return "
%s
" % {download_started, download_finished, v } end function add_custom(details) for _, d in ipairs(details) do -- refactor: swap favicon (custom1) and tags (custom2) if d["custom1"]:ends(".ico") or d["custom1"]:ends(".png") then local temp = d["custom1"] d["custom1"] = d["custom2"] d["custom2"] = temp rtorrent.call("d.custom1.set", d["hash"], d["custom1"]) rtorrent.call("d.custom2.set", d["hash"], d["custom2"]) end d["status"] = status(d) d["done_percent"] = 100.0 * d["bytes_done"] / d["size_bytes"] d["eta"] = eta(d) d["icon"] = favicon(d) d["custom1"] = (d["custom1"] == "") and "all" or d["custom1"] end end function add_summary(details) table.insert(details, { ["name"] = "TOTAL: " .. total["name"] .. " pcs.", ["size_bytes"] = common.human_size(total["size_bytes"]), ["down_rate"] = string.format("%.2f", total["down_rate"] / 1000), ["up_rate"] = string.format("%.2f", total["up_rate"] / 1000), ["select"] = "%hidden%" }) end function html_format(details) table.sort(details, function(a, b) return a["name"] < b["name"] end) for _, d in ipairs(details) do for m, v in pairs(d) do d[m] = format[m] and format[m](d, v) or tostring(v) end end end f = SimpleForm("rtorrent", translate("Torrent List")) f.reset = false f.submit = false local details = rtorrent.multicall("d.", "default", unpack(methods)) add_custom(details) local tags = get_tags(details) local user = luci.dispatcher.context.authuser local page = arg[1] or (has_tag(tags, user) and user or "all") local filtered = filter(details, page) html_format(filtered) if #filtered > 1 then add_summary(filtered) end t = f:section(Table, filtered) t.template = "rtorrent/list" t.pages = tags t.page = page t.headcol = 2 AbstractValue.tooltip = function(self, s) self.hint = s return self end t:option(DummyValue, "icon").rawhtml = true t:option(DummyValue, "name", translate("Name")).rawhtml = true t:option(DummyValue, "size_bytes", translate("Size")):tooltip("Full size of torrent").rawhtml = true t:option(DummyValue, "done_percent", translate("Done")):tooltip("Download done percent").rawhtml = true t:option(DummyValue, "status", translate("Status")).rawhtml = true t:option(DummyValue, "peers_accounted", "↑"):tooltip("Seeder count").rawhtml = true t:option(DummyValue, "peers_complete", "↓"):tooltip("Leecher count").rawhtml = true t:option(DummyValue, "down_rate", translate("Down
Speed")):tooltip("Download speed in kb/s").rawhtml = true t:option(DummyValue, "up_rate", translate("Up
Speed")):tooltip("Upload speed in kb/s").rawhtml = true t:option(DummyValue, "ratio", translate("Ratio")):tooltip("Download/upload ratio").rawhtml = true t:option(DummyValue, "eta", translate("ETA")):tooltip("Estimated Time of Arrival").rawhtml = true select = t:option(Flag, "select") select.template = "rtorrent/fvalue" function select.write(self, section, value) table.insert(selected, filtered[section].hash) end s = f:section(SimpleSection) s.template = "rtorrent/buttonsection" s.style = "float: right;" start = s:option(Button, "start", translate("Start")) start.template = "rtorrent/button" start.inputstyle = "apply" function start.write(self, section, value) if next(selected) ~= nil then for _, hash in ipairs(selected) do rtorrent.call("d.open", hash) rtorrent.call("d.start", hash) end luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) end end stop = s:option(Button, "stop", translate("Stop")) stop.template = "rtorrent/button" stop.inputstyle = "reset" function stop.write(self, section, value) if next(selected) ~= nil then for _, hash in ipairs(selected) do rtorrent.call("d.stop", hash) rtorrent.call("d.close", hash) end luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) end end remove = s:option(Button, "remove", translate("Remove")) remove.template = "rtorrent/button" remove.inputstyle = "remove" function remove.write(self, section, value) if next(selected) ~= nil then for _, hash in ipairs(selected) do rtorrent.call("d.close", hash) rtorrent.call("d.erase", hash) end luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) end end r = f:section(SimpleSection) r.template = "rtorrent/buttonsection" r.style = "float: right;" delete = r:option(Button, "delete", translate("Remove and delete data")) delete.template = "rtorrent/button" delete.inputstyle = "remove" function delete.write(self, section, value) if next(selected) ~= nil then for _, hash in ipairs(selected) do rtorrent.call("d.custom5.set", hash, "1") rtorrent.call("d.close", hash) rtorrent.call("d.erase", hash) end luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) end end return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/rss-rule.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local rtorrent = require "rtorrent" m = Map("rtorrent") m.redirect = luci.dispatcher.build_url("admin/rtorrent/rss") local name = m:get(arg[1], "name") m.title = "%s - %s" %{"RSS Downloader", name or "(Unnamed rule)"} s = m:section(NamedSection, arg[1], "rss-rule") s.anonymous = true s.addremove = false enabled = s:option(Flag, "enabled", "Enabled") enabled.rmempty = false name = s:option(Value, "name", "Name") name.rmempty = false match = s:option(TextValue, "match", "Match (lua regex)") match.template = "rtorrent/tvalue" match.rmempty = false match.rows = 1 exclude = s:option(TextValue, "exclude", "Exclude (lua regex)") exclude.rows = 1 s:option(Value, "minsize", "Min size (MiB):") s:option(Value, "maxsize", "Max size (MiB):") feed = s:option(MultiValue, "feed", "Feed") feed.delimiter = ";" m.uci:foreach(m.config, "rss-feed", function(f) feed:value(f.name, f.name) end) tags = s:option(Value, "tags", "Add tags") tags.default = "all" destdir = s:option(Value, "destdir", "Download directory") destdir.default = rtorrent.call("directory.default") destdir.datatype = "directory" destdir.rmempty = false autostart = s:option(Flag, "autostart", "Start download") autostart.default = "1" autostart.rmempty = false return m ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/rss.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. m = Map("rtorrent", "RSS Downloader") s = m:section(TypedSection, "rss-rule") s.addremove = true s.anonymous = true s.sortable = true s.template = "cbi/tblsection" s.extedit = luci.dispatcher.build_url("admin/rtorrent/rss/%s") s.template_addremove = "rtorrent/rss_addrule" function s.parse(self, ...) TypedSection.parse(self, ...) local newrule_name = m:formvalue("_newrule.name") local newrule_submit = m:formvalue("_newrule.submit") if newrule_submit then newrule = TypedSection.create(self, section) self.map:set(newrule, "name", newrule_name) m.uci:save("rtorrent") luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/rss", newrule)) end end name = s:option(DummyValue, "name", "Name") name.width = "30%" match = s:option(DummyValue, "match", "Match") match.width = "60%" enabled = s:option(Flag, "enabled", "Enabled") enabled.rmempty = false return m ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/string.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. function string.starts(str, begin) if not str then return false end return string.sub(str, 1, string.len(begin)) == begin end function string.ends(str, tail) if not str then return false end return string.sub(str, -string.len(tail)) == tail end function string.split(str, sep) if sep == nil then sep = "%s" end local t = {} for s in str:gmatch("([^" .. sep .. "]+)") do table.insert(t, s) end return t end function string.ucfirst(str) return (str:gsub("^%l", string.upper)) end function string.trim(str) return str:match("^()%s*$") and "" or str:match("^%s*(.*%S)") end ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local rtorrent = require "rtorrent" local nixio = require "nixio" local common = require "luci.model.cbi.rtorrent.common" local hash = arg[1] local details = rtorrent.batchcall({"name", "base_path"}, hash, "d.") local files = rtorrent.multicall("f.", hash, 0, "path", "path_depth", "path_components", "size_bytes", "size_chunks", "completed_chunks", "priority", "frozen_path") local format, total = {}, {} function format.icon(r, v) local icon_path = "/luci-static/resources/icons/filetypes" local ext = v:match("%.([^%.]+)$") if ext and nixio.fs.stat("/www/%s/%s.png" % {icon_path, ext:lower()}, "type") then return "%s/%s.png" % {icon_path, ext:lower()} end return "%s/%s.png" % {icon_path, "file"} end function format.dir(r, v) return " " .. v end function format.file(r, v) total["name"] = (total["name"] or 0) + 1 local url = luci.dispatcher.build_url("admin/rtorrent/download/") .. nixio.bin.b64encode(r["frozen_path"]) local link = r["chunks_percent"] == 100 and "" .. v .. "" or v return " " .. link end function format.size_bytes(r, v) total["size_bytes"] = (total["size_bytes"] or 0) + v return common.human_size(v) end function format.chunks_percent(r, v) return common.div(string.format("%.1f%%", v), v < 100 and "red") end function format.priority(r, v) return tostring(v) end function add_custom(files) for i, r in ipairs(files) do r["id"] = i r["chunks_percent"] = r["completed_chunks"] * 100.0 / r["size_chunks"] end end function add_summary(list) table.insert(list, { ["name"] = (translate("TOTAL").. ": ".. total["name"]), ["size_bytes"] = common.human_size(total["size_bytes"]), ["priority"] = "%hidden%" }) end function path_compare(a, b) if a["path_depth"] ~= b["path_depth"] and (a["path_depth"] == 1 or b["path_depth"] == 1) then return a["path_depth"] > b["path_depth"] end return a["path"] < b["path"] end local list, last_path = {}, {} add_custom(files) table.sort(files, path_compare) for _, r in ipairs(files) do for i, p in ipairs(r["path_components"]) do if last_path[i] ~= p then local t = i == #r["path_components"] and "file" or "dir" local n = {} if t == "file" then for m, v in pairs(r) do n[m] = format[m] and format[m](r, v) or tostring(v) end else n["priority"] = "%hidden%" end n["name"] = string.rep(" ", i - 1) .. format[t](r, p) table.insert(list, n) end last_path[i] = p end end f = SimpleForm("rtorrent", details["name"]) f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") if nixio.fs.stat(details["base_path"], "type") == "dir" and table.getn(list) > 1 then f.cancel = "Download all" else f.cancel = false end if #list > 1 then add_summary(list) end t = f:section(Table, list) t.template = "rtorrent/list" t.pages = common.get_torrent_pages(hash) t.page = "File List" AbstractValue.tooltip = function(self, s) self.hint = s return self end t:option(DummyValue, "name", translate("Name")).rawhtml = true t:option(DummyValue, "size_bytes", translate("Size")) t:option(DummyValue, "chunks_percent", translate("Done")):tooltip("Download done percent").rawhtml = true prio = t:option(ListValue, "priority", translate("Priority")):tooltip("Rotate priority") prio.template = "rtorrent/lvalue" prio.onclick = [[ var inputs = document.getElementsByClassName("cbi-input-select"); for (var i = 0; i < inputs.length; i++) { if (inputs[i].selectedIndex < inputs[i].length - 1) { inputs[i].selectedIndex++; } else { inputs[i].selectedIndex = 0; } } ]] prio:value("0", "off") prio:value("1", "normal") prio:value("2", "high") function prio.write(self, section, value) rtorrent.call("f.priority.set", hash .. ":f" .. (list[tonumber(section)].id - 1), tonumber(value)) luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/files/%s" % hash)) end function f:on_cancel() luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/downloadall/") .. nixio.bin.b64encode(details["base_path"])) end return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local rtorrent = require "rtorrent" local common = require "luci.model.cbi.rtorrent.common" local hash = arg[1] local details = rtorrent.batchcall({"name", "custom1", "timestamp.started", "timestamp.finished"}, hash, "d.") f = SimpleForm("rtorrent", details["name"]) f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") t = f:section(Table, list) t.template = "rtorrent/list" t.pages = common.get_torrent_pages(hash) t.page = "Info" hash_id = f:field(DummyValue, "hash", translate("Hash")) function hash_id.cfgvalue(self, section) return hash end started = f:field(DummyValue, "started", translate("Download started")) started.value = details["timestamp.started"] == 0 and "not yet started" or os.date("!%Y-%m-%d %H:%M:%S", details["timestamp.started"]) finished = f:field(DummyValue, "finished", translate("Download finished")) finished.value = details["timestamp.finished"] == 0 and "not yet finished" or os.date("!%Y-%m-%d %H:%M:%S", details["timestamp.finished"]) tags = f:field(Value, "tags", translate("Tags")) tags.default = details["custom1"] tags.rmempty = false function tags.write(self, section, value) rtorrent.call("d.custom1.set", hash, value) end function f.handle(self, state, data) if state == FORM_VALID then luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/info/") .. hash) end return true end return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local rtorrent = require "rtorrent" local http = require "socket.http" local common = require "luci.model.cbi.rtorrent.common" local hash = arg[1] local details = rtorrent.batchcall({"name"}, hash, "d.") local format, total, map = {}, {}, {} local geoplugin_net = { address = "http://www.geoplugin.net/json.gp?ip=%s", fields = { geoplugin_countryCode = "country_code", geoplugin_countryName = "country", geoplugin_region = "region", geoplugin_city = "city", geoplugin_latitude = "latitude", geoplugin_longitude = "longitude" }} local ip2geo = geoplugin_net function map.googlemap(latitude, longitude, zoom) return "https://google.com/maps/place/%s,%s/@%s,%s,%sz" % {latitude, longitude, latitude, longitude, zoom} end function map.openstreetmap(latitude, longitude, zoom) return "http://www.openstreetmap.org/?mlat=%s&mlon=%s#map=%s/%s/%s/m" % {latitude, longitude, zoom, latitude, longitude} end function format.address(r, v) total["address"] = (total["address"] or 0) + 1 local map = map.googlemap(r.latitude, r.longitude, 11) -- local map = map.openstreetmap(r.latitude, r.longitude, 11) -- local flag = "" % r.country_code:lower() -- local flag = "" % r.country_code:lower() local flag = "" % r.country_code:lower() return "%s %s" % {flag, map, v} end function format.completed_percent(r, v) return string.format("%.1f%%", v) end function format.down_rate(d, v) total["down_rate"] = (total["down_rate"] or 0) + v return string.format("%.2f", v / 1000) end function format.up_rate(d, v) total["up_rate"] = (total["up_rate"] or 0) + v return string.format("%.2f", v / 1000) end function format.down_total(d, v) return "
%s
" % {v, common.human_size(v)} end function format.up_total(d, v) return format.down_total(d, v) end function json2table(json) loadstring("j2t = " .. string.gsub(string.gsub(json, '([,%{])%s*\n?%s*"', '%1["'), '"%s*:%s*', '"]='))() return j2t end function add_location(r) for i, j in pairs(json2table(http.request(ip2geo.address % r.address))) do if ip2geo.fields[i] then r[ip2geo.fields[i]] = j end end local location = {} for _, k in ipairs({"country", "region", "city"}) do if r[k] ~= "" and not tonumber(r[k]) then table.insert(location, (r[k]:gsub("\u(%x%x%x%x)", "&#x%1"))) end end r["location"] = table.concat(location, "/") end function add_summary(list) table.insert(list, { ["address"] = (translate("TOTAL").. ": " .. total["address"]), ["down_rate"] = string.format("%.2f", total["down_rate"] / 1000), ["up_rate"] = string.format("%.2f", total["up_rate"] / 1000) }) end local list = rtorrent.multicall("p.", hash, 0, "address", "completed_percent", "client_version", "down_rate", "up_rate", "up_total", "down_total") for _, r in ipairs(list) do add_location(r) for k, v in pairs(r) do r[k] = format[k] and format[k](r, v) or tostring(v) end end f = SimpleForm("rtorrent", details["name"]) f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") f.reset = false f.submit = false if #list > 1 then add_summary(list) end t = f:section(Table, list) t.template = "rtorrent/list" t.pages = common.get_torrent_pages(hash) t.page = "Peer List" AbstractValue.tooltip = function(self, s) self.hint = s return self end t:option(DummyValue, "address", translate("Address")):tooltip("Peer IP address").rawhtml = true t:option(DummyValue, "client_version", translate("Client")):tooltip("Client version") t:option(DummyValue, "location", translate("Location")):tooltip("Location: country/region/city").rawhtml = true t:option(DummyValue, "completed_percent", translate("Done")):tooltip("Download done percent") t:option(DummyValue, "down_rate", translate("Down
Speed")):tooltip("Download speed in kb/s") t:option(DummyValue, "up_rate", translate("Up
Speed")):tooltip("Upload speed in kb/s") t:option(DummyValue, "down_total", translate("Downloaded")):tooltip("Total downloaded").rawhtml = true t:option(DummyValue, "up_total", translate("Uploaded")):tooltip("Total uploaded").rawhtml = true return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local rtorrent = require "rtorrent" local common = require "luci.model.cbi.rtorrent.common" local hash = arg[1] local details = rtorrent.batchcall({"name"}, hash, "d.") local format, total = {}, {} function format.url(r, v) total["url"] = (total["url"] or 0) + 1 end function format.scrape_downloaded(r, v) total["scrape_downloaded"] = (total["scrape_downloaded"] or 0) + v return tostring(v) end function format.scrape_complete(r, v) total["scrape_complete"] = (total["scrape_complete"] or 0) + v return tostring(v) end function format.scrape_incomplete(r, v) total["scrape_incomplete"] = (total["scrape_incomplete"] or 0) + v return tostring(v) end function format.scrape_time_last(r, v) return common.human_time(os.time() - v) end function add_summary(list) table.insert(list, { ["url"] = (translate("TOTAL").. ": " .. total["url"]), ["scrape_downloaded"] = tostring(total["scrape_downloaded"]), ["scrape_complete"] = tostring(total["scrape_complete"]), ["scrape_incomplete"] = tostring(total["scrape_incomplete"]), ["is_enabled"] = "%hidden%" }) end local list = rtorrent.multicall("t.", hash, 0, "is_enabled", "url", "scrape_downloaded", "scrape_complete", "scrape_incomplete", "scrape_time_last") for _, r in ipairs(list) do for k, v in pairs(r) do r[k] = format[k] and format[k](r, v) or tostring(v) end end f = SimpleForm("rtorrent", details["name"]) f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") if #list > 1 then add_summary(list) end t = f:section(Table, list) t.template = "rtorrent/list" t.pages = common.get_torrent_pages(hash) t.page = "Tracker List" AbstractValue.tooltip = function(self, s) self.hint = s return self end t:option(DummyValue, "url", translate("Url")) t:option(DummyValue, "scrape_downloaded", translate("D")):tooltip("Downloaded") t:option(DummyValue, "scrape_complete", translate("S")):tooltip("Seeders") t:option(DummyValue, "scrape_incomplete", translate("L")):tooltip("Leechers") t:option(DummyValue, "scrape_time_last", translate("Updated")):tooltip("Last update time").rawhtml = true enabled = t:option(Flag, "is_enabled", translate("Enabled")) enabled.template = "rtorrent/fvalue" enabled.rmempty = false enabled.rawhtml = true function enabled.write(self, section, value) if value ~= tostring(list[section].is_enabled) then rtorrent.call("t.is_enabled.set", hash .. ":t" .. (section - 1), tonumber(value)) luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/trackers/%s" % hash)) end end add = f:field(Value, "add_tracker", translate("Add tracker")) function add.write(self, section, value) rtorrent.call("d.tracker.insert", hash, table.getn(list), value) luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/trackers/%s" % hash)) end return f ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/button.htm ================================================ <%+cbi/valueheader%> " type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. ifattr(self.style, "style") .. attr("value", self:cfgvalue(section) or self.inputtitle or self.title)%> /> <%+cbi/valuefooter%> ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/buttonsection.htm ================================================
> <% self:render_children(1, { valueheader = "rtorrent/empty", valuefooter = "rtorrent/empty" }) %>
================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/empty.htm ================================================ ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/fvalue.htm ================================================ <%+cbi/valueheader%> <% if not self:cfgvalue(section) or self:cfgvalue(section) ~= "%hidden%" then -%> /> /> <%- end %> <%+cbi/valuefooter%> ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/list.htm ================================================ <%- local rowcnt = 1 function rowstyle() rowcnt = rowcnt + 1 return (rowcnt % 2) + 1 end -%> <%+rtorrent/tabmenu%>
<%=self.description%>
<%- for i, k in pairs(self.children) do -%>
<%=ifattr(k.onclick, "onclick", k.onclick)%>><%=k.title%>
<%- end -%>
<%- for j, k in ipairs(self:cfgsections()) do section = k scope = { valueheader = "rtorrent/empty", valuefooter = "rtorrent/empty" } -%>
<%- for i, node in ipairs(self.children) do -%>
"> <%- node:render(section, scope or {}) -%>
">
<%- end -%>
<%- end -%>
================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/lvalue.htm ================================================ <%+cbi/valueheader%> <% if not self:cfgvalue(section) or self:cfgvalue(section) ~= "%hidden%" then -%> <% if self.widget == "select" then %> <% elseif self.widget == "radio" then local c = 0 for i, key in pairs(self.keylist) do c = c + 1 %> /> ><%=self.vallist[i]%> <% if c == self.size then c = 0 %><% if self.orientation == "horizontal" then %> <% else %>
<% end %> <% end end %> <% end %> <%- end %> <%+cbi/valuefooter%> ================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/rss_addrule.htm ================================================
================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/tabmenu.htm ================================================
    <%- for i, k in ipairs(self.pages) do -%> <%- if k.name:lower() ~= "protected" then if self.page:lower() == k.name:lower() then -%>
  • <%=k.name%>
  • <%- else -%>
  • <%=k.name%>
  • <%- end end -%> <%- end -%>
================================================ FILE: luci/applications/luci-app-rtorrent/luasrc/view/rtorrent/tvalue.htm ================================================ <%+cbi/tvalue%> ================================================ FILE: luci/applications/luci-app-rtorrent/po/en/rtorrent.po ================================================ msgid "" msgstr "" "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:10 msgid "Torrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:11 msgid "Torrent List" msgstr "" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:12 msgid "Add Torrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:13 msgid "RSS Downloader" msgstr "" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:14 msgid "Torrent Settings" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:11 msgid "Add Torrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:16 msgid "Torrent
or magnet URI" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:32 msgid "Upload torrent file" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:42 msgid "Download directory" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:47 msgid "Tags" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:52 msgid "Start now" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:79 msgid "Incomplete" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:138 msgid "not yet started" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:140 msgid "not yet finished" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:182 msgid "Torrent List" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:204 msgid "Name" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:205 msgid "Size" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:206 msgid "Done" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:207 msgid "Status" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:210 msgid "Down
Speed" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:211 msgid "Up
Speed" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:212 msgid "Ratio" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:213 msgid "ETA" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:226 msgid "Start" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:240 msgid "Stop" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:254 msgid "Remove" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:272 msgid "Remove and delete data" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:22 msgid "Admin - rTorrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:25 msgid "Bandwidth limits" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:34 msgid "Upload limit (KiB/sec)" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:35 msgid "Global upload rate (0: unlimited)" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:43 msgid "Download limit (KiB/sec)" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:44 msgid "Global downlaod rate (0: unlimited)" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:54 msgid "Global limits" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:56 msgid "Download slots" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:57 msgid "Maximum number of simultaneous downloads" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:65 msgid "Upload slots" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:66 msgid "Maximum number of simultaneous uploads" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:76 msgid "Torrent limits" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:78 msgid "Maximum uploads" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:79 msgid "Maximum number of simultanious uploads per torrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:87 msgid "Minimum peers" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:88 msgid "Minimum number of peers to connect to per torrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:96 msgid "Maximum peers" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:97 msgid "Maximum number of peers to connect to per torrent" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:105 msgid "Minimum seeds" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:106 msgid "Minimum number of seeds for completed torrents (-1 = same as peers)" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:114 msgid "Maximum seeds" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:115 msgid "Maximum number of seeds for completed torrents (-1 = same as peers)" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:57 msgid "TOTAL" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:108 msgid "Name" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:110 msgid "Done" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:111 msgid "Priority" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:18 msgid "Hash" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:23 msgid "Download started" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:28 msgid "Download finished" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:33 msgid "Tags" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:80 msgid "TOTAL" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:109 msgid "Address" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:110 msgid "Client" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:111 msgid "Location" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:112 msgid "Done" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:113 msgid "Down
Speed" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:114 msgid "Up
Speed" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:115 msgid "Downloaded" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:116 msgid "Uploaded" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:37 msgid "TOTAL" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:65 msgid "Url" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:66 msgid "D" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:67 msgid "S" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:68 msgid "L" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:69 msgid "Updated" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:71 msgid "Enabled" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:83 msgid "Add tracker" msgstr "" ================================================ FILE: luci/applications/luci-app-rtorrent/po/ru/rtorrent.po ================================================ msgid "" msgstr "" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlakov \n" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:10 msgid "Torrent" msgstr "Торрент" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:11 msgid "Torrent List" msgstr "Список торрентов" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:12 msgid "Add Torrent" msgstr "Добавить торрент" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:13 msgid "RSS Downloader" msgstr "Лента RSS" #: applications/luci-app-rtorrent/luasrc/controller/rtorrent.lua:14 msgid "Torrent Settings" msgstr "Настройки Торрента" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:11 msgid "Add Torrent" msgstr "Добавить торрент" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:16 msgid "Torrent
or magnet URI" msgstr "Торрент
или magnet-ссылка" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:32 msgid "Upload torrent file" msgstr "Загрузить файл" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:42 msgid "Download directory" msgstr "Директория загрузок" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:47 msgid "Tags" msgstr "Тег" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/add.lua:52 msgid "Start now" msgstr "Запустить сейчас" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:79 msgid "Incomplete" msgstr "Незавершено" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:138 msgid "not yet started" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:140 msgid "not yet finished" msgstr "" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:182 msgid "Torrent List" msgstr "Список загрузок" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:204 msgid "Name" msgstr "Имя" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:205 msgid "Size" msgstr "Размер" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:206 msgid "Done" msgstr "Завершено" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:207 msgid "Status" msgstr "Статус" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:210 msgid "Down
Speed" msgstr "Входящ.
Скорость" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:211 msgid "Up
Speed" msgstr "Исх.
Скорость" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:212 msgid "Ratio" msgstr "Рейтинг" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:213 msgid "ETA" msgstr "Осталось" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:226 msgid "Start" msgstr "Пуск" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:240 msgid "Stop" msgstr "Стоп" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:254 msgid "Remove" msgstr "Удалить" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/main.lua:272 msgid "Remove and delete data" msgstr "Удалить данные" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:22 msgid "Admin - rTorrent" msgstr "Настройки rTorrent" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:25 msgid "Bandwidth limits" msgstr "Ограничения скорости" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:34 msgid "Upload limit (KiB/sec)" msgstr "Лимит отдачи (KiB/sec)" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:35 msgid "Global upload rate (0: unlimited)" msgstr "Общий лимит отдачи (0: без ограничений)" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:43 msgid "Download limit (KiB/sec)" msgstr "Лимит загрузки (KiB/sec)" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:44 msgid "Global downlaod rate (0: unlimited)" msgstr "Общий лимит загрузки (0: без ограничений)" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:54 msgid "Global limits" msgstr "Общие ограничения" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:56 msgid "Download slots" msgstr "Слоты загрузки" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:57 msgid "Maximum number of simultaneous downloads" msgstr "Максимальное кооличество одновременных загрузок" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:65 msgid "Upload slots" msgstr "Слоты отдачи" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:66 msgid "Maximum number of simultaneous uploads" msgstr "Максимальное количество одновременных раздач" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:76 msgid "Torrent limits" msgstr "Ограничения торрента" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:78 msgid "Maximum uploads" msgstr "Максимум потоков" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:79 msgid "Maximum number of simultanious uploads per torrent" msgstr "Максимальное количество потоков для оного торрента" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:87 msgid "Minimum peers" msgstr "Минимальное кол-во пиров" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:88 msgid "Minimum number of peers to connect to per torrent" msgstr "Минимальное количество пиров для торрента" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:96 msgid "Maximum peers" msgstr "Максимальное кол-во пиров" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:97 msgid "Maximum number of peers to connect to per torrent" msgstr "Максимально количество пиров для одного торрента" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:105 msgid "Minimum seeds" msgstr "Минимальное кол-во сидов" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:106 msgid "Minimum number of seeds for completed torrents (-1 = same as peers)" msgstr "Минимальное количество сидов для одного торрента (-1 = согласно пирам)" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:114 msgid "Maximum seeds" msgstr "Максимус сидов" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/admin/rtorrent.lua:115 msgid "Maximum number of seeds for completed torrents (-1 = same as peers)" msgstr "Максимальное количество сидов для одного торрента (-1 = согласно пирам)" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:57 msgid "TOTAL" msgstr "Всего" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:108 msgid "Name" msgstr "Имя" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:110 msgid "Done" msgstr "Завершено" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/files.lua:111 msgid "Priority" msgstr "Приоритет" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:18 msgid "Hash" msgstr "Хеш" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:23 msgid "Download started" msgstr "Загрузка начата" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:28 msgid "Download finished" msgstr "Загрузка окончена" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/info.lua:33 msgid "Tags" msgstr "Тег" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:80 msgid "TOTAL" msgstr "Всего" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:109 msgid "Address" msgstr "Адрес" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:110 msgid "Client" msgstr "Клиент" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:111 msgid "Location" msgstr "Место" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:112 msgid "Done" msgstr "Завершено" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:113 msgid "Down
Speed" msgstr "Входящ.
Скорость" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:114 msgid "Up
Speed" msgstr "Исх.
Скорость" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:115 msgid "Downloaded" msgstr "Загружено" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/peers.lua:116 msgid "Uploaded" msgstr "Отдано" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:37 msgid "TOTAL" msgstr "Всего" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:65 msgid "Url" msgstr "Адрес" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:66 msgid "D" msgstr "Загр." #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:67 msgid "S" msgstr "Сиды" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:68 msgid "L" msgstr "Личи" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:69 msgid "Updated" msgstr "Обновлен" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:71 msgid "Enabled" msgstr "Включен" #: applications/luci-app-rtorrent/luasrc/model/cbi/rtorrent/torrent/trackers.lua:83 msgid "Add tracker" msgstr "Добавить трекер" ================================================ FILE: luci/applications/luci-app-rtorrent/root/etc/config/rtorrent ================================================ ================================================ FILE: luci/applications/luci-app-rtorrent/root/etc/cookies.txt ================================================ # Netscape Cookie File # From left-to-right, the cookie data consists of the following fields: # domain - The domain that created and that can read the variable. # tailmatch - A TRUE/FALSE value indicating if all machines within a given domain can access the variable. # path - The path within the domain that the variable is valid for. Use / for any url. # secure - A TRUE/FALSE value indicating if a secure connection with the domain is needed to access the variable. # expiration - The UNIX time that the variable will expire on, max: 2147483647 (2038-01-19). # name - The name of the variable. # value - The value of the variable. # domain tailmatch path secure expiration name value .netscape.com TRUE / FALSE 2147483647 dummy 1234 ================================================ FILE: luci/applications/luci-app-rtorrent/root/etc/init.d/rtorrent ================================================ #!/bin/sh /etc/rc.common START=99 STOP=99 start() { screen -dmS rtorrent nice -19 rtorrent -n -o import=/etc/rtorrent.conf } boot() { start } stop() { killall rtorrent } ================================================ FILE: luci/applications/luci-app-rtorrent/root/etc/rtorrent.conf ================================================ # This is an example resource file for rTorrent. # Enable/modify the options as needed. Remember to uncomment # the options you wish to enable. # Default directory to save the downloaded torrents #directory = /path/to/downloads/ # Default session directory #session = /path/to/session/ # rTorrent xml-rpc scgi port # Use the same port in luci-app-rtorrent configuration! scgi_port = 127.0.0.1:5000 # Port range to use for listening port_range = 21244-21244 port_random = no # Enable DHT support for trackerless torrents or when all trackers are down. # May be set to "disable" (completely disable DHT), "off" (do not start DHT), # "auto" (start and stop DHT as needed), or "on" (start DHT immediately). # The default is "off". For DHT to work, a session directory must be defined. dht = auto dht_port = 6881 # Enable tracker requests trackers.enable = 1 # Set whether the client should try to connect to UDP trackers trackers.use_udp.set = true # Enable peer exchange (for torrents not marked private) protocol.pex.set = yes # Global upload and download rate in KiB. "0" for unlimited #throttle.global_down.max_rate.set_kb = 0 #throttle.global_up.max_rate.set_kb = 0 # Maximum number of simultaneous downloads/uploads #throttle.max_downloads.global.set = 0 #throttle.max_uploads.global.set = 0 # Maximum number of simultanious uploads per torrent #throttle.max_uploads.set = 20 # Maximum and minimum number of peers to connect to per torrent #throttle.min_peers.normal.set = 40 #throttle.max_peers.normal.set = 100 # Same as above but for seeding completed torrents (-1 = same as downloading) #throttle.min_peers.seed.set = 10 #throttle.max_peers.seed.set = 50 # Check hash for finished torrents check_hash = no # Encryption options, set to none (default) or any combination of the following: # allow_incoming, try_outgoing, require, require_RC4, enable_retry, prefer_plaintext encryption = allow_incoming,try_outgoing,enable_retry # Set the umask applied to all files created by rTorrent system.umask.set = 022 # Support non-ascii characters in the filenames encoding_list = UTF-8 # Maximum number of socket connections rtorrent can accept/make #network.max_open_sockets.set = 300 # Maximum number of open files rtorrent can keep open #network.max_open_files.set = 600 # Maximum number of simultaneous HTTP request #network.http.max_open.set = 50 # CURL option to lower DNS timeout #network.http.dns_cache_timeout.set = 25 # Max packet size using xmlrpc #network.xmlrpc.size_limit.set = 2M # rTorrent scheduler/events schedule = rss_downloader,300,300,"execute2=/usr/lib/lua/rss_downloader.lua" method.set_key = event.download.erased,on_erase,"branch=d.custom5=,\"execute2={rm,-rf,--,$d.base_path=}\"" # Logging #log.execute = /path/to/log/rtorrent.execute.log #log.xmlrpc = /path/to/log/rtorrent.xmlrpc.log ================================================ FILE: luci/applications/luci-app-rtorrent/root/etc/uci-defaults/rtorrent ================================================ #!/bin/sh /etc/init.d/rtorrent enable /etc/init.d/rtorrent start rm -fr /tmp/luci-indexcache /tmp/luci-modulecache exit 0 ================================================ FILE: luci/applications/luci-app-rtorrent/root/usr/lib/lua/bencode.lua ================================================ -- Copyright (c) 2009, 2010, 2011, 2012 by Moritz Wilhelmy -- Copyright (c) 2009 by Kristofer Karlsson -- Public domain lua-module for handling bittorrent-bencoded data. -- This module includes both a recursive decoder and a recursive encoder. local sort, concat, insert = table.sort, table.concat, table.insert local pairs, ipairs, type, tonumber = pairs, ipairs, type, tonumber local sub, find = string.sub, string.find local M = {} -- helpers local function islist(t) local n = #t for k, v in pairs(t) do if type(k) ~= "number" or k % 1 ~= 0 -- integer? or k < 1 or k > n then return false end end for i = 1, n do if t[i] == nil then return false end end return true end -- encoder functions local encode_rec -- encode_list/dict and encode_rec are mutually recursive... local function encode_list(t, x) insert(t, "l") for _,v in ipairs(x) do local err,ev = encode_rec(t, v); if err then return err,ev end end insert(t, "e") end local function encode_dict(t, x) insert(t, "d") -- bittorrent requires the keys to be sorted. local sortedkeys = {} for k, v in pairs(x) do if type(k) ~= "string" then return "bencoding requires dictionary keys to be strings", k end insert(sortedkeys, k) end sort(sortedkeys) for k, v in ipairs(sortedkeys) do local err,ev = encode_rec(t, v); if err then return err,ev end err,ev = encode_rec(t, x[v]); if err then return err,ev end end insert(t, "e") end local function encode_int(t, x) if x % 1 ~= 0 then return "number is not an integer", x end insert(t, "i" ) insert(t, x ) insert(t, "e" ) end local function encode_str(t, x) insert(t, #x ) insert(t, ":" ) insert(t, x ) end encode_rec = function(t, x) local typx = type(x) if typx == "string" then return encode_str (t, x) elseif typx == "number" then return encode_int (t, x) elseif typx == "table" then if islist(x) then return encode_list (t, x) else return encode_dict (t, x) end else return "type cannot be converted to an acceptable type for bencoding", typx end end -- call recursive bencoder function with empty table, stringify that table. -- this is the only encode* function visible to module users. M.encode = function (x) local t = {} local err, val = encode_rec(t,x) if not err then return concat(t) else return nil, err, val end end -- decoder functions local function decode_integer(s, index) local a, b, int = find(s, "^(%-?%d+)e", index) if not int then return nil, "not a number", nil end int = tonumber(int) if not int then return nil, "not a number", int end return int, b + 1 end local function decode_list(s, index) local t = {} while sub(s, index, index) ~= "e" do local obj, ev obj, index, ev = M.decode(s, index) if not obj then return obj, index, ev end insert(t, obj) end index = index + 1 return t, index end local function decode_dictionary(s, index) local t = {} while sub(s, index, index) ~= "e" do local obj1, obj2, ev obj1, index, ev = M.decode(s, index) if not obj1 then return obj1, index, ev end obj2, index, ev = M.decode(s, index) if not obj2 then return obj2, index, ev end t[obj1] = obj2 end index = index + 1 return t, index end local function decode_string(s, index) local a, b, len = find(s, "^([0-9]+):", index) if not len then return nil, "not a length", len end index = b + 1 local v = sub(s, index, index + len - 1) if #v < tonumber(len) then return nil, "truncated string at end of input", v end index = index + len return v, index end M.decode = function (s, index) if not s then return nil, "no data", nil end index = index or 1 local t = sub(s, index, index) if not t then return nil, "truncation error", nil end if t == "i" then return decode_integer(s, index + 1) elseif t == "l" then return decode_list(s, index + 1) elseif t == "d" then return decode_dictionary(s, index + 1) elseif t >= '0' and t <= '9' then return decode_string(s, index) else return nil, "invalid type", t end end return M ================================================ FILE: luci/applications/luci-app-rtorrent/root/usr/lib/lua/rss_downloader.lua ================================================ #!/usr/bin/lua -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local uci = require "luci.model.uci".cursor() local date = require "luci.http.protocol.date" local bencode = require "bencode" local xml = require "lxp.lom" local xmlrpc = require "xmlrpc" local rtorrent = require "rtorrent" local nixio = require "nixio" local common = require "luci.model.cbi.rtorrent.common" require "luci.model.cbi.rtorrent.string" CONFIG = "rtorrent" function log(str) print(os.date("%Y-%m-%d %H:%M:%S") .. " " .. str) end function uci_get_all_sections(config, stype, filter) local sections = {} uci:foreach(config, stype, function(s) if not filter or filter(s) then table.insert(sections, s) end end) return sections end function get_feeds(filter) return uci_get_all_sections(CONFIG, "rss-feed", filter) end function get_rules(filter) return uci_get_all_sections(CONFIG, "rss-rule", filter) end function filter_enabled(s) return s.enabled == "1" end function next_tag(t, tag, i) if not i then i = 1 end if t ~= nil then while t[i] do if type(t[i]) == "table" and t[i].tag == tag then return t[i], i end i = i + 1 end end return nil, i end function parse_feed(url) local ok, content = common.get(url) if ok and content ~= nil then local rss, err = xml.parse(content) if rss ~= nil then return rss else log("Failed to parse rss feed: " .. err) end else log("Failed to download rss feed: " .. url) end end function contains(tbl, value) for _, v in ipairs(tbl) do if v == value then return true end end return false end function get_torrent_size(torrent) local sha1_size = 20 local t, err = bencode.decode(torrent) if t then local piece_length = tonumber(t["info"]["piece length"]) local piece_count = t["info"]["pieces"]:len() / sha1_size return piece_length * piece_count / 1024 / 1024 else log("Failed to parse torrent file: " .. tostring(torrent)) return nil end end function get_torrent_link(item) local enclosure = next_tag(item, "enclosure") if enclosure == nil then return next_tag(item, "link")[1] end return enclosure.attr.url end --[[ M A I N ]]-- -- TODO: fix character encoding (eg.: lua expat does not support iso8859-2) -- string.gsub(rss, "[^\128-\193]", "") local feed_logfile = uci:get(CONFIG, "logging", "feed_logfile") if feed_logfile ~= nil then feed_log = assert(io.open(feed_logfile, "a")) end local rules = get_rules(filter_enabled) for _, feed in ipairs(get_feeds(filter_enabled)) do log("Processing \"" .. feed.name .. "\" rss feed") local rss = parse_feed(feed.url) local channel = next_tag(rss, "channel") local item, i = next_tag(channel, "item") local lastupdate = tonumber(feed.lastupdate) or 0 if item then uci:set(CONFIG, feed[".name"], "lastupdate", date.to_unix(next_tag(item, "pubDate")[1])) uci:save(CONFIG) uci:commit(CONFIG) end while item do local pubdate = date.to_unix(next_tag(item, "pubDate")[1]) if pubdate > lastupdate then local title = next_tag(item, "title")[1] if feed_log ~= nil then feed_log:write(os.date("!%Y-%m-%d %H:%M:%S", pubdate) .. " (" .. feed.name .. ") " .. title .. "\n") end for _, rule in ipairs(rules) do if rule.feed and contains(rule.feed:split(";"), feed.name) and title:lower():find(rule.match:lower()) and (not rule.exclude or not title:lower():find(rule.exclude:lower())) then local link = get_torrent_link(item) local ok, torrent = common.get(link) local size = get_torrent_size(torrent) if ok and size and (not rule.minsize or (size >= tonumber(rule.minsize))) and (not rule.maxsize or (size <= tonumber(rule.maxsize))) then log("Matched rss rule: " .. rule.name .. " (" .. link .. ")") local params = {} table.insert(params, rule.autostart == "1" and "load.raw_start" or "load.raw") table.insert(params, "") -- target table.insert(params, xmlrpc.newTypedValue((nixio.bin.b64encode(torrent)), "base64")) table.insert(params, "d.directory.set=\"" .. rule.destdir .. "\"") if rule.tags then table.insert(params, "d.custom1.set=\"" .. rule.tags .. "\"") end table.insert(params, "d.custom3.set=" .. nixio.bin.b64encode(link)) rtorrent.call(unpack(params)) end end end end item, i = next_tag(channel, "item", i + 1) end end if feed_log ~= nil then feed_log:flush() feed_log:close() end ================================================ FILE: luci/applications/luci-app-rtorrent/root/usr/lib/lua/rtorrent.lua ================================================ -- Copyright 2014-2018 Sandor Balazsi -- Licensed to the public under the GNU General Public License. local ipairs, string, tostring, tonumber, table = ipairs, string, tostring, tonumber, table local assert, type, unpack = assert, type, unpack local nixio = require "nixio" local socket = require "socket" local xmlrpc = require "xmlrpc" local scgi = require "xmlrpc.scgi" local SCGI_ADDRESS = "localhost" local SCGI_PORT = 5000 local HTTP_AUTH_USER = "rtorrent" local HTTP_AUTH_PASSWORD = "czNz9JwdcLYcGDcVnZbQ" module "rtorrent" function map(array, func) local new_array = {} for i, v in ipairs(array) do new_array[i] = func(v) end return new_array end function alter(prefix, methods, postfix) methods = map(methods, function(method) if method == 0 then return method end if prefix then method = prefix .. method end if postfix then method = method .. postfix end return method end) return methods end function format(method_type, res, methods) local formatted = {} for _, r in ipairs(res) do local item = {} for i, v in ipairs(r) do item[methods[method_type == "d." and i or i + 1]:gsub("%.", "_")] = v end table.insert(formatted, item) end return formatted end function call(method, ...) local ok, res = scgi.call(SCGI_ADDRESS, SCGI_PORT, method, ...) if not ok and res == "socket connect failed" then assert(ok, "\n\nFailed to connect to rtorrent: rpc port not reachable!\n" .. "Possible reasons:\n" .. "- not the rpc version of rtorrent is installed\n" .. "- scgi port is not defined in .rtorrent.rc (scgi_port = 127.0.0.1:5000)\n" .. "- rtorrent is not running (ps | grep rtorrent)\n") end assert(ok, string.format("XML-RPC call failed on client: %s", tostring(res))) return res end function rpc(xml) local auth = "Basic " .. nixio.bin.b64encode(HTTP_AUTH_USER .. ":" .. HTTP_AUTH_PASSWORD) if auth ~= nixio.getenv("HTTP_AUTHORIZATION") then return 'Status: 401 Unauthorized\r\n' .. 'WWW-Authenticate: Basic realm="rTorrent"\r\n\r\n' end local sock = socket.connect(SCGI_ADDRESS, SCGI_PORT) if sock ~= nil then sock:send(scgi.netstring(xml)) local err, code, headers, body = scgi.receive(sock) if tonumber(code) == 200 then return 'Status: 200 OK\r\n' .. 'Content-Type: application/xml\r\n\r\n' .. body end end return 'Status: 500 Internal Server Error\r\n\r\n' end function multicall(method_type, filter, ...) local res = (method_type == "d.") and call(method_type .. "multicall2", "", filter, unpack(alter(method_type, {...}, "="))) or call(method_type .. "multicall", filter, unpack(alter(method_type, {...}, "="))) return format(method_type, res, {...}) end function batchcall(methods, params, prefix, postfix) local p = type(params) == "table" and params or { params } local methods_array = {} for _, m in ipairs(alter(prefix, methods, postfix)) do table.insert(methods_array, { ["methodName"] = m, ["params"] = xmlrpc.newTypedValue(p, "array") }) end local res = {} for i, r in ipairs(call("system.multicall", xmlrpc.newTypedValue(methods_array, "array"))) do res[methods[i]] = r[1] end return res end ================================================ FILE: luci/applications/luci-app-rtorrent/root/usr/lib/lua/xmlrpc/init.lua ================================================ -- Copyright 2003-2010 Kepler Project -- XML-RPC implementation for Lua. local lxp = require "lxp" local lom = require "lxp.lom" local assert, error, ipairs, pairs, select, type, tonumber, unpack = assert, error, ipairs, pairs, select, type, tonumber, unpack local format, gsub, strfind, strsub = string.format, string.gsub, string.find, string.sub local concat, tinsert = table.concat, table.insert local ceil = math.ceil local parse = lom.parse module (...) _COPYRIGHT = "Copyright (C) 2003-2014 Kepler Project" _DESCRIPTION = "LuaXMLRPC is a library to make remote procedure calls using XML-RPC" _PKGNAME = "LuaXMLRPC" _VERSION_MAJOR = 1 _VERSION_MINOR = 2 _VERSION_MICRO = 2 _VERSION = _VERSION_MAJOR .. "." .. _VERSION_MINOR .. "." .. _VERSION_MICRO --------------------------------------------------------------------- -- XML-RPC Parser --------------------------------------------------------------------- --------------------------------------------------------------------- local function trim (s) return (type(s) == "string" and gsub (s, "^%s*(.-)%s*$", "%1")) end --------------------------------------------------------------------- local function is_space (s) return type(s) == "string" and trim(s) == "" end --------------------------------------------------------------------- -- Get next non-space element from tab starting from index i. -- @param tab Table. -- @param i Numeric index. -- @return Object and its position on table; nil and an invalid index -- when there is no more elements. --------------------------------------------------------------------- function next_nonspace (tab, i) if not i then i = 1 end while is_space (tab[i]) do i = i+1 end return tab[i], i end --------------------------------------------------------------------- -- Get next element of tab with the given tag starting from index i. -- @param tab Table. -- @param tag String with the name of the tag. -- @param i Numeric index. -- @return Object and its position on table; nil and an invalid index -- when there is no more elements. --------------------------------------------------------------------- local function next_tag (tab, tag, i) if not i then i = 1 end while tab[i] do if type (tab[i]) == "table" and tab[i].tag == tag then return tab[i], i end i = i + 1 end return nil, i end --------------------------------------------------------------------- local function x2number (tab) if tab.tag == "int" or tab.tag == "i4" or tab.tag == "i8" or tab.tag == "double" then return tonumber (next_nonspace (tab, 1), 10) end end --------------------------------------------------------------------- local function x2boolean (tab) if tab.tag == "boolean" then local v = next_nonspace (tab, 1) return v == true or v == "true" or tonumber (v) == 1 or false end end --------------------------------------------------------------------- local function x2string (tab) return tab.tag == "string" and (tab[1] or "") end --------------------------------------------------------------------- local function x2date (tab) return tab.tag == "dateTime.iso8601" and next_nonspace (tab, 1) end --------------------------------------------------------------------- local function x2base64 (tab) return tab.tag == "base64" and next_nonspace (tab, 1) end --------------------------------------------------------------------- local function x2name (tab) return tab.tag == "name" and next_nonspace (tab, 1) end local x2value --------------------------------------------------------------------- -- Disassemble a member object in its name and value parts. -- @param tab Table with a DOM representation. -- @return String (name) and Object (value). -- @see x2name, x2value. --------------------------------------------------------------------- local function x2member (tab) return x2name (next_tag(tab,"name")), x2value (next_tag(tab,"value")) end --------------------------------------------------------------------- -- Disassemble a struct object into a Lua table. -- @param tab Table with DOM representation. -- @return Table with "name = value" pairs. --------------------------------------------------------------------- local function x2struct (tab) if tab.tag == "struct" then local res = {} for i = 1, #tab do if not is_space (tab[i]) then local name, val = x2member (tab[i]) res[name] = val end end return res end end --------------------------------------------------------------------- -- Disassemble an array object into a Lua table. -- @param tab Table with DOM representation. -- @return Table. --------------------------------------------------------------------- local function x2array (tab) if tab.tag == "array" then local d = next_tag (tab, "data") local res = {} for i = 1, #d do if not is_space (d[i]) then tinsert (res, x2value (d[i])) end end return res end end --------------------------------------------------------------------- local xmlrpc_types = { int = x2number, i4 = x2number, i8 = x2number, boolean = x2boolean, string = x2string, double = x2number, ["dateTime.iso8601"] = x2date, base64 = x2base64, struct = x2struct, array = x2array, } local x2param, x2fault --------------------------------------------------------------------- -- Disassemble a methodResponse into a Lua object. -- @param tab Table with DOM representation. -- @return Boolean (indicating wether the response was successful) -- and (a Lua object representing the return values OR the fault -- string and the fault code). --------------------------------------------------------------------- local function x2methodResponse (tab) assert (type(tab) == "table", "Not a table") assert (tab.tag == "methodResponse", "Not a `methodResponse' tag: "..tab.tag) local t = next_nonspace (tab, 1) if t.tag == "params" then return true, unpack (x2param (t)) elseif t.tag == "fault" then local f = x2fault (t) return false, f.faultString, f.faultCode else error ("Couldn't find a nor a element") end end --------------------------------------------------------------------- -- Disassemble a value element into a Lua object. -- @param tab Table with DOM representation. -- @return Object. --------------------------------------------------------------------- x2value = function (tab) local t = tab.tag assert (t == "value", "Not a `value' tag: "..t) local n = next_nonspace (tab) if type(n) == "string" or type(n) == "number" then return n elseif type (n) == "table" then local t = n.tag local get = xmlrpc_types[t] if not get then error ("Invalid <"..t.."> element") end return get (next_nonspace (tab)) elseif type(n) == "nil" then -- the next best thing is to assume it's an empty string return "" end end --------------------------------------------------------------------- -- Disassemble a fault element into a Lua object. -- @param tab Table with DOM representation. -- @return Object. --------------------------------------------------------------------- x2fault = function (tab) assert (tab.tag == "fault", "Not a `fault' tag: "..tab.tag) return x2value (next_nonspace (tab)) end --------------------------------------------------------------------- -- Disassemble a param element into a Lua object. -- Ignore white spaces between elements. -- @param tab Table with DOM representation. -- @return Object. --------------------------------------------------------------------- x2param = function (tab) assert (tab.tag == "params", "Not a `params' tag") local res = {} local p, i = next_nonspace (tab, 1) while p do if p.tag == "param" then tinsert (res, x2value (next_tag (p, "value"))) end p, i = next_nonspace (tab, i+1) end return res end --------------------------------------------------------------------- -- Disassemble a methodName element into a Lua object. -- @param tab Table with DOM representation. -- @return Object. --------------------------------------------------------------------- local function x2methodName (tab) assert (tab.tag == "methodName", "Not a `methodName' tag: "..tab.tag) return (next_nonspace (tab, 1)) end --------------------------------------------------------------------- -- Disassemble a methodCall element into its name and a list of parameters. -- @param tab Table with DOM representation. -- @return Object. --------------------------------------------------------------------- local function x2methodCall (tab) assert (tab.tag == "methodCall", "Not a `methodCall' tag: "..tab.tag) return x2methodName (next_tag (tab,"methodName")), x2param (next_tag (tab,"params")) end --------------------------------------------------------------------- -- End of XML-RPC Parser --------------------------------------------------------------------- --------------------------------------------------------------------- -- Convert a Lua Object into an XML-RPC string. --------------------------------------------------------------------- --------------------------------------------------------------------- local formats = { boolean = "%d", number = "%d", string = "%s", base64 = "%s", array = "\n%s\n", double = "%s", int = "%s", struct = "%s", member = "%s%s", value = "%s", param = "%s", params = [[ %s ]], fault = [[ %s ]], methodCall = [[ %s %s ]], methodResponse = [[ %s ]], } formats.table = formats.struct local toxml = {} toxml.double = function (v,t) return format (formats.double, v) end toxml.int = function (v,t) return format (formats.int, v) end toxml.string = function (v,t) return format (formats.string, v) end toxml.base64 = function (v,t) return format (formats.base64, v) end --------------------------------------------------------------------- -- Build a XML-RPC representation of a boolean. -- @param v Object. -- @return String. --------------------------------------------------------------------- function toxml.boolean (v) local n = (v and 1) or 0 return format (formats.boolean, n) end --------------------------------------------------------------------- -- Build a XML-RPC representation of a number. -- @param v Object. -- @param t Object representing the XML-RPC type of the value. -- @return String. --------------------------------------------------------------------- function toxml.number (v, t) local tt = (type(t) == "table") and t["*type"] if tt == "int" or tt == "i4" or tt == "i8" then return toxml.int (v, t) elseif tt == "double" then return toxml.double (v, t) elseif v == ceil(v) then return toxml.int (v, t) else return toxml.double (v, t) end end --------------------------------------------------------------------- -- @param typ Object representing a type. -- @return Function that generate an XML element of the given type. -- The object could be a string (as usual in Lua) or a table with -- a field named "type" that should be a string with the XML-RPC -- type name. --------------------------------------------------------------------- local function format_func (typ) if type (typ) == "table" then return toxml[typ.type] else return toxml[typ] end end --------------------------------------------------------------------- -- @param val Object representing an array of values. -- @param typ Object representing the type of the value. -- @return String representing the equivalent XML-RPC value. --------------------------------------------------------------------- function toxml.array (val, typ) local ret = {} local et = typ.elemtype local f = format_func (et) for i,v in ipairs (val) do if et and et ~= "array" then tinsert (ret, format (formats.value, f (v, et))) else local ct,cv = type_val(v) local cf = format_func(ct) tinsert (ret, format (formats.value, cf(cv, ct))) end end return format (formats.array, concat (ret, '\n')) end --------------------------------------------------------------------- --------------------------------------------------------------------- function toxml.struct (val, typ) local ret = {} if type (typ) == "table" then for n,t in pairs (typ.elemtype) do local f = format_func (t) tinsert (ret, format (formats.member, n, f (val[n], t))) end else for i, v in pairs (val) do tinsert (ret, toxml.member (i, v)) end end return format (formats.struct, concat (ret)) end toxml.table = toxml.struct --------------------------------------------------------------------- --------------------------------------------------------------------- function toxml.member (n, v) return format (formats.member, n, toxml.value (v)) end --------------------------------------------------------------------- -- Get type and value of object. --------------------------------------------------------------------- function type_val (obj) local t = type (obj) local v = obj if t == "table" then t = obj["*type"] or "table" v = obj["*value"] or obj end return t, v end --------------------------------------------------------------------- -- Convert a Lua object to a XML-RPC object (plain string). --------------------------------------------------------------------- function toxml.value (obj) local to, val = type_val (obj) if type(to) == "table" then return format (formats.value, toxml[to.type] (val, to)) else -- primitive (not structured) types. --return format (formats[to], val) return format (formats.value, toxml[to] (val, to)) end end --------------------------------------------------------------------- -- @param ... List of parameters. -- @return String representing the `params' XML-RPC element. --------------------------------------------------------------------- function toxml.params (...) local params_list = {} for i = 1, select ("#", ...) do params_list[i] = format (formats.param, toxml.value (select (i, ...))) end return format (formats.params, concat (params_list, '\n ')) end --------------------------------------------------------------------- -- @param method String with method's name. -- @param ... List of parameters. -- @return String representing the `methodCall' XML-RPC element. --------------------------------------------------------------------- function toxml.methodCall (method, ...) local idx = strfind (method, "[^A-Za-z_.:/0-9]") if idx then error (format ("Invalid character `%s'", strsub (method, idx, idx))) end return format (formats.methodCall, method, toxml.params (...)) end --------------------------------------------------------------------- -- @param err String with error message. -- @return String representing the `fault' XML-RPC element. --------------------------------------------------------------------- function toxml.fault (err) local code local message = err if type (err) == "table" then code = err.code message = err.message end return format (formats.fault, toxml.value { faultCode = { ["*type"] = "int", ["*value"] = code or err.faultCode or 1 }, faultString = message or err.faultString or "fatal error", }) end --------------------------------------------------------------------- -- @param ok Boolean indicating if the response was correct or a -- fault one. -- @param params Object containing the response contents. -- @return String representing the `methodResponse' XML-RPC element. --------------------------------------------------------------------- function toxml.methodResponse (ok, params) local resp if ok then resp = toxml.params (params) else resp = toxml.fault (params) end return format (formats.methodResponse, resp) end --------------------------------------------------------------------- -- End of converter from Lua to XML-RPC. --------------------------------------------------------------------- --------------------------------------------------------------------- -- Create a representation of an array with the given element type. --------------------------------------------------------------------- function newArray (elemtype) return { type = "array", elemtype = elemtype, } end --------------------------------------------------------------------- -- Create a representation of a structure with the given members. --------------------------------------------------------------------- function newStruct (members) return { type = "struct", elemtype = members, } end --------------------------------------------------------------------- -- Create a representation of a value according to a type. -- @param val Any Lua value. -- @param typ A XML-RPC type. --------------------------------------------------------------------- function newTypedValue (val, typ) return { ["*type"] = typ, ["*value"] = val } end --------------------------------------------------------------------- -- Create the XML-RPC string used to call a method. -- @param method String with method name. -- @param ... Parameters to the call. -- @return String with the XML string/document. --------------------------------------------------------------------- function clEncode (method, ...) return toxml.methodCall (method, ...) end --------------------------------------------------------------------- -- Convert the method response document to a Lua table. -- @param meth_resp String with XML document. -- @return Boolean indicating whether the call was successful or not; -- and a Lua object with the converted response element. --------------------------------------------------------------------- function clDecode (meth_resp) local d = parse (meth_resp) if type(d) ~= "table" then error ("Not an XML document: "..meth_resp) end return x2methodResponse (d) end --------------------------------------------------------------------- -- Convert the method call (client request) document to a name and -- a list of parameters. -- @param request String with XML document. -- @return String with method's name AND the table of arguments. --------------------------------------------------------------------- function srvDecode (request) local d = parse (request) if type(d) ~= "table" then error ("Not an XML document: "..request) end return x2methodCall (d) end --------------------------------------------------------------------- -- Convert a table into an XML-RPC methodReponse element. -- @param obj Lua object. -- @param is_fault Boolean indicating wether the result should be -- a `fault' element (default = false). -- @return String with XML-RPC response. --------------------------------------------------------------------- function srvEncode (obj, is_fault) local ok = not (is_fault or false) return toxml.methodResponse (ok, obj) end --------------------------------------------------------------------- -- Register the methods. -- @param tab_or_func Table or mapping function. -- If a table is given, it can have one level of objects and then the -- methods; -- if a function is given, it will be used as the dispatcher. -- The given function should return a Lua function that implements. --------------------------------------------------------------------- dispatch = error function srvMethods (tab_or_func) local t = type (tab_or_func) if t == "function" then dispatch = tab_or_func elseif t == "table" then dispatch = function (name) local ok, _, obj, method = strfind (name, "^([^.]+)%.(.+)$") if not ok then return tab_or_func[name] else if tab_or_func[obj] and tab_or_func[obj][method] then return function (...) return tab_or_func[obj][method] (obj, ...) end else return nil end end end else error ("Argument is neither a table nor a function") end end ================================================ FILE: luci/applications/luci-app-rtorrent/root/usr/lib/lua/xmlrpc/scgi.lua ================================================ -- Copyright 2003-2010 Kepler Project -- Copyright 2014-2018 Sandor Balazsi -- XML-RPC over SCGI. local error, tonumber, tostring, unpack = error, tonumber, tostring, unpack local socket= require"socket" local string= require"string" local xmlrpc= require"xmlrpc" module("xmlrpc.scgi") --------------------------------------------------------------------- -- Call a remote method. -- @param addr String with the address of the SCGI server. -- @param port The port of the SCGI server. -- @param method String with the name of the method to be called. -- @return Table with the response(could be a `fault' or a `params' -- XML-RPC element). --------------------------------------------------------------------- function call(addr, port, method, ...) local request_body = xmlrpc.clEncode(method, ...) local sock = socket.connect(addr, port) if sock == nil then return false, "socket connect failed" end sock:send(netstring(request_body)) local err, code, headers, body = receive(sock) if tonumber(code) == 200 then return xmlrpc.clDecode(body) else error(tostring(err or code)) end end --------------------------------------------------------------------- -- Encode message as netstring -- @param request_body String with the message -- @return String with the encoded message --------------------------------------------------------------------- function netstring(request_body) local null = "\0" local content_length = "CONTENT_LENGTH" .. null .. string.len(request_body) .. null local scgi_enable = "SCGI" .. null .. "1" .. null local request_method = "REQUEST_METHOD" .. null .. "POST" .. null local server_protocol = "SERVER_PROTOCOL" .. null .. "HTTP/1.1" .. null local header = content_length .. scgi_enable .. request_method .. server_protocol return string.len(header) .. ":" .. header .. "," .. request_body end --------------------------------------------------------------------- -- Receive and parse socket response -- @param sock Socket instance -- @return Headers, body and error codes --------------------------------------------------------------------- function receive(sock) local line, body, err local headers = {} line, err = sock:receive() if err then return err, "500" end while line ~= "" do local name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) if not(name and value) then return "malformed reponse header: " .. line, "500" end headers[string.lower(name)] = value line, err = sock:receive() if err then return err, "500" end end body = sock:receive(headers["content-length"]) local code = socket.skip(2, string.find(headers["status"], "^(%d%d%d)")) return err, code, headers, body end ================================================ FILE: luci/applications/luci-app-smstools3/Makefile ================================================ include $(TOPDIR)/rules.mk LUCI_TITLE:=Web UI for smstools3 LUCI_DEPENDS:=+smstools3 +iconv +jq PKG_LICENSE:=GPLv3 PKG_VERSION:=0.1.3 PKG_RELEASE:=4 define Package/luci-app-smstools3/postrm rm -f /etc/config/smstools3 endef include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-smstools3/README.md ================================================ # luci-app-smstools3 Web UI smstools3 for OpenWrt LuCI. Note: If you use this app with modemmanager, please move or remove /etc/hotplug.d/tty/25-modemmanager-tty
Screenshots ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-smstools3/screenshots/incoming.png) ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-smstools3/screenshots/outcoming.png) ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-smstools3/screenshots/push.png) ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/applications/luci-app-smstools3/screenshots/setup.png)
================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/cmd.js ================================================ 'use strict'; 'require form'; 'require rpc'; 'require fs'; 'require view'; 'require uci'; 'require ui'; 'require tools.widgets as widgets' return view.extend({ load: function() { return uci.load('smstools3'); }, render: function(data) { var m, s, o; m = new form.Map('smstools3', _('Smstools3: Commands')); m.description = _('Command List interface.'); // Root Phone numbers s = m.section(form.TypedSection, 'root_phone', _('Phone'), _('Root Phone list to accept commands.')); s.rmempty = true; s.anonymous = true; o = s.option(form.DynamicList, 'phone', _('Phone'), _('Phone number must be without \"+\"')); o.rmempty = true; // Grid Section modems s = m.section(form.GridSection, 'command', _('Command List')); s.addremove = true; s.rmempty = true; // get modem list var modemOptions = [['', _('Any modem (default)')]]; var sections = uci.sections('smstools3', 'modem'); o = s.option(form.ListValue, 'modem', _('Modem')); for (var i = 0; i < sections.length; i++) { var section = sections[i]; if (section['enable'] === '1') { modemOptions.push([section['.name'], '%s (%s)'.format(section['.name'], section['device'] || 'N/A')]); } } for (var i = 0; i < modemOptions.length; i++) { o.value(modemOptions[i][0], modemOptions[i][1]); } o.default = ''; o.rmempty = true; o = s.option(form.Value, 'command', _('SMS Command')); o = s.option(form.Value, 'exec', _('Execute')); o = s.option(form.Flag, 'delay_en', _('Delay')); o = s.option(form.Value, 'delay', _('Delay in sec.')); o.depends('delay_en', '1'); o = s.option(form.Flag, 'answer_en', _('Answer')); o = s.option(form.Value, 'answer', _('Answer MSG')); o.depends('answer_en', '1'); return m.render(); } }); ================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/in.js ================================================ 'use strict'; 'require dom'; 'require form'; 'require fs'; 'require ui'; 'require uci'; 'require view'; /* Copyright 2025 Konstantine Shevlakov Licensed to the GNU General Public License v3.0. */ return view.extend({ load: function() { L.resolveDefault(fs.exec_direct('/usr/share/luci-app-smstools3/led.sh', [ 'off' ])); return L.resolveDefault(fs.exec_direct('/usr/bin/msg_control', [ 'recv' ])); }, handleClear: function(ev) { return L.resolveDefault(fs.exec_direct('/usr/bin/msg_control', [ 'rmrecv' ])).then(function() { location.reload(); }); }, handleDelete: function(filename) { if (confirm(_('Are you sure you want to delete this message?'))) { return L.resolveDefault(fs.exec_direct('/usr/bin/msg_control', [ 'delete', filename ])).then(function() { location.reload(); }); } }, handleRefresh: function(ev) { location.reload(); }, render: function (data) { var obj = JSON.parse(data); let tableHeaders = [ _('Modem'), _('Send Date'), _('Recv.Date'), _('From'), _('Message'), _('Action') ]; let tableSMS = E('table', { 'class': 'table' }, E('tr', { 'class': 'tr table-titles' }, [ E('th', { 'class': 'th left', 'width': '10%' }, tableHeaders[0]), E('th', { 'class': 'th left', 'width': '10%' }, tableHeaders[1]), E('th', { 'class': 'th left', 'width': '10%' }, tableHeaders[2]), E('th', { 'class': 'th left', 'width': '20%' }, tableHeaders[3]), E('th', { 'class': 'th left', 'width': '45%' }, tableHeaders[4]), E('th', { 'class': 'th left', 'width': '5%' }, tableHeaders[5]), ]), ); var s = 1; for (let i = 0; i < obj.recv.length; i++) { if (obj.recv[i].from.length > 6 && Number(obj.recv[i].from)) { var from = '+' + obj.recv[i].from; } else { var from = obj.recv[i].from; } // get msg filename var filename = obj.recv[i].filename || ''; tableSMS.append(E('tr', { 'class': 'tr cbi-rowstyle-'+s }, [ E('td', { 'class': 'td left', 'data-title': tableHeaders[0], 'width': '10%' }, obj.recv[i].modem), E('td', { 'class': 'td left', 'data-title': tableHeaders[1], 'width': '10%' }, obj.recv[i].srecv), E('td', { 'class': 'td left', 'data-title': tableHeaders[2], 'width': '10%' }, obj.recv[i].drecv), E('td', { 'class': 'td left', 'data-title': tableHeaders[3], 'width': '20%' }, from), E('td', { 'class': 'td left', 'data-title': tableHeaders[4], 'width': '45%' }, obj.recv[i].content), E('td', { 'class': 'td left', 'data-title': tableHeaders[5], 'width': '5%' }, E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(this, 'handleDelete', obj.recv[i].filename), 'title': _('Delete this message') }, [ '×' ]) ) ]), ); s = (s % 2) + 1; }; var button = ( E('hr'), E('div', { 'class': 'right' }, [ E('button', { 'class': 'cbi-button cbi-button-remove', 'id': 'clr', 'click': ui.createHandlerFn(this, 'handleClear') }, [ _('Remove All SMS') ]), '\xa0\xa0\xa0', E('button', { 'class': 'cbi-button cbi-button-save', 'id': 'clr', 'click': ui.createHandlerFn(this, 'handleRefresh') }, [ _('Refresh') ]) ]) ); var result = E('fieldset', { 'class': 'cbi-section' }, [E('h2', {}, _('Smstools3: Incoming messages')), tableSMS, button]); return result; }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/out.js ================================================ 'use strict'; 'require dom'; 'require form'; 'require fs'; 'require ui'; 'require uci'; 'require view'; /* Copyright 2025 Konstantine Shevlakov Licensed to the GNU General Public License v3.0. */ return view.extend({ load: function() { return L.resolveDefault(fs.exec_direct('/usr/bin/msg_control', [ 'sent' ])); }, handleClear: function(ev) { return L.resolveDefault(fs.exec_direct('/usr/bin/msg_control', [ 'rmsent' ])).then(function() { location.reload(); }); }, handleDelete: function(filename) { if (confirm(_('Are you sure you want to delete this message?'))) { return L.resolveDefault(fs.exec_direct('/usr/bin/msg_control', [ 'delete', filename ])).then(function() { location.reload(); }); } }, handleRefresh: function(ev) { location.reload(); }, render: function (data) { var obj = JSON.parse(data); let tableHeaders = [ _('Modem'), _('Send Date'), _('Time(sec)'), _('To'), _('Message'), _('Action') ]; let tableSMS = E('table', { 'class': 'table' }, E('tr', { 'class': 'tr cbi-section-table-titles' }, [ E('th', { 'class': 'th left', 'width': '12%' }, tableHeaders[0]), E('th', { 'class': 'th left', 'width': '12%' }, tableHeaders[1]), E('th', { 'class': 'th left', 'width': '12%' }, tableHeaders[2]), E('th', { 'class': 'th left', 'width': '18%' }, tableHeaders[3]), E('th', { 'class': 'th left', 'width': '41%' }, tableHeaders[4]), E('th', { 'class': 'th left', 'width': '5%' }, tableHeaders[5]), ]), ); var s = 1; for (let i = 0; i < obj.sent.length; i++) { let message = obj.sent[i]; if (!message.filename) { continue; } let to = message.to; if (to && to.length > 6 && Number(to)) { to = '+' + to; } else { to = to || ''; } let content = message.content || _('No content'); tableSMS.append( E('tr', { 'class': 'cbi-rowstyle-'+s }, [ E('td', { 'class': 'td left', 'data-title': tableHeaders[0] }, message.modem || ''), E('td', { 'class': 'td left', 'data-title': tableHeaders[1] }, message.sent || ''), E('td', { 'class': 'td left', 'data-title': tableHeaders[2] }, message.time || ''), E('td', { 'class': 'td left', 'data-title': tableHeaders[3] }, to), E('td', { 'class': 'td left', 'data-title': tableHeaders[4] }, E('div', { 'style': 'max-height: 100px; overflow-y: auto; word-wrap: break-word;' }, content)), E('td', { 'class': 'td left', 'data-title': tableHeaders[5] }, E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(this, 'handleDelete', message.filename), 'title': _('Delete this message') }, [ '×' ]) ) ]), ); s = (s % 2) + 1; }; var button = ( E('hr'), E('div', { 'class': 'right' }, [ E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(this, 'handleClear') }, [ _('Remove All SMS') ]), '\xa0\xa0\xa0', E('button', { 'class': 'cbi-button cbi-button-save', 'click': ui.createHandlerFn(this, 'handleRefresh') }, [ _('Refresh') ]) ]) ); var result = E('fieldset', { 'class': 'cbi-section' }, [ E('h2', {}, _('Smstools3: Outgoing messages')), tableSMS, button ]); return result; }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/pb.js ================================================ 'use strict'; 'require form'; 'require fs'; 'require view'; 'require uci'; 'require ui'; 'require tools.widgets as widgets' /* Copyright 2022-2023 Rafał Wabik - IceG - From eko.one.pl forum Modified for smstools3 by Konstantine Shevlakov 2024 Licensed to the GNU General Public License v3.0. */ var cmddesc = _("Each line must have the following format: 'Description;Phone Number'. For user convenience, the file is saved to the location /etc/smstools3.pb."); return view.extend({ render: function() { var m, s, o; m = new form.Map('smstools3', _('Smstools3: Phonebook')); s = m.section(form.TypedSection, 'sms', '', _('')); s.anonymous = true; o = s.option(form.TextValue, '_tmpl', _('Phonebook'), cmddesc); o.rows = 20; o.cfgvalue = function(section_id) { return fs.trimmed('/etc/smstools3.pb'); }; o.write = function(section_id, formvalue) { return fs.write('/etc/smstools3.pb', formvalue.trim().replace(/\r\n/g, '\n') + '\n'); }; return m.render(); }, handleSaveApply: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/script.js ================================================ 'use strict'; 'require form'; 'require fs'; 'require view'; 'require uci'; 'require ui'; 'require tools.widgets as widgets' return view.extend({ load: function(){ uci.load('smstools3'); }, render: function(data) { var desc_head = _('Edit smstools3 user script. Add user\'s actions for incoming and outcoming messages.
Is shell script for smstools3 scenario. See \smstools3 manual page\ for more details.'); var config = uci.sections('smstools3'); var m, s, o; m = new form.Map('smstools3', _('Smtools3: User Script'), desc_head); s = m.section(form.TypedSection, 'sms', null); s.anonymous = true; o = s.option(form.TextValue, '_tmpl', _('Edit User script smstools3.
File stored in /etc/smstools3.user')); o.rows = 20; o.cfgvalue = function(section_id) { return fs.trimmed('/etc/smstools3.user'); }; o.write = function(section_id, formvalue) { return fs.write(('/etc/smstools3.user'), formvalue.trim().replace(/\r\n/g, '\n') + '\n'); }; return m.render(); }, handleSaveApply: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/send.js ================================================ 'use strict'; 'require dom'; 'require form'; 'require fs'; 'require uci'; 'require ui'; 'require view'; /* Copyright 2022-2023 Rafal Wabik - IceG - From eko.one.pl forum Modified for smstools3 by Konstantine Shevlakov 2025 Licensed to the GNU General Public License v3.0. */ return view.extend({ handleCommand: function(exec, args) { var buttons = document.querySelectorAll('.cbi-button'); for (var i = 0; i < buttons.length; i++) buttons[i].setAttribute('disabled', 'true'); return fs.exec(exec, args).then(function(res) { res.stdout = res.stdout?.replace(/(?=\n)$|\s*|\s*$|\n\n+/gm, "") || ''; res.stderr = res.stderr?.replace(/(?=\n)$|\s*|\s*$|\n\n+/gm, "") || ''; }).catch(function(err) { ui.addNotification(null, E('p', [ err ])) }).finally(function() { for (var i = 0; i < buttons.length; i++) buttons[i].removeAttribute('disabled'); }); }, handleGo: function(ev) { var smstext = document.getElementById('textvalue').value; var phone = document.getElementById('phonevalue').value; var modem = document.getElementById('modemvalue').value; if ( phone.length < 2 ) { ui.addNotification(null, E('p', _('Please specify the phone number to send')), 'info'); return false; } else if ( phone.length < 6 && Number(phone) && !phone.startsWith('+') ) { // send short numbers ui.addNotification(null, E('p', _('Message sent')), 'info'); return this.handleCommand('/usr/bin/send_sms', [ 's'+phone, smstext, modem ]); } else { ui.addNotification(null, E('p', _('Message sent')), 'info'); return this.handleCommand('/usr/bin/send_sms', [ phone, smstext, modem ]); } }, handleClear: function(ev) { var ov = document.getElementById('phonevalue'); ov.value = ''; var ov = document.getElementById('textvalue'); ov.value = ''; document.getElementById('textvalue').focus(); }, handleCopy: function(ev) { var ov = document.getElementById('phonevalue'); ov.value = ''; var x = document.getElementById('tk').value; ov.value = x; }, load: function() { return Promise.all([ L.resolveDefault(fs.read_direct('/etc/smstools3.pb'), null), uci.load('smstools3') ]); }, render: function (loadResults) { var info = _('User interface for sending SMS via smsd.'); var execBtn = document.getElementById('execute'); // get defined modems from uci var modems = []; var sections = uci.sections('smstools3', 'modem'); for (var i = 0; i < sections.length; i++) { var section = sections[i]; // check enabled modems if (section['enable'] === '1') { modems.push({ name: section['.name'], device: section['device'] || 'N/A' }); } } // if modems not defined or found if (modems.length === 0) { for (var i = 0; i < sections.length; i++) { var section = sections[i]; modems.push({ name: section['.name'], device: section['device'] || 'N/A' }); } } return E('div', { 'class': 'cbi-map', 'id': 'map' }, [ E('h2', {}, [ _('Smstools3: send message') ]), E('div', { 'class': 'cbi-map-descr'}, info), E('hr'), E('div', { 'class': 'cbi-section' }, [ E('div', { 'class': 'cbi-section-node' }, [ E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('Modem') ]), E('div', { 'class': 'cbi-value-field' }, [ E('select', { 'class': 'cbi-input-select', 'id': 'modemvalue', 'style': 'margin:5px 0; width:70%;' }, modems.map(function(modem) { return E('option', { 'value': modem.name }, [ '%s (%s)'.format(modem.name, modem.device) ]); })) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('Phonebook') ]), E('div', { 'class': 'cbi-value-field' }, [ E('select', { 'class': 'cbi-input-select', 'id': 'tk', 'style': 'margin:5px 0; width:70%;', 'change': ui.createHandlerFn(this, 'handleCopy') }, (loadResults[0] || "").trim().split("\n").map(function(cmd) { var fields = cmd.split(/;/); var name = fields[0]; var code = fields[1]; return E('option', { 'value': code }, name ) }) ) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('Phone Number') ]), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'style': 'margin:5px 0; ; width:70%;', 'type': 'text', 'id': 'phonevalue', }), ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, [ _('Text Message') ]), E('div', { 'class': 'cbi-value-field' }, [ E('textarea', { 'class': 'cbi-section', 'style': 'margin:600px 20; ; width:70%;', 'type': 'text', 'rows': '6', 'id': 'textvalue' }), ]) ]), ]) ]), E('hr'), E('div', { 'class': 'right' }, [ E('button', { 'class': 'cbi-button cbi-button-remove', 'id': 'clr', 'click': ui.createHandlerFn(this, 'handleClear') }, [ _('Clear text') ]), '\xa0\xa0\xa0', E('button', { 'class': 'cbi-button cbi-button-action important', 'id': 'execute', 'click': ui.createHandlerFn(this, 'handleGo') }, [ _('Send message') ]), ]), ]); }, handleSaveApply: null, handleSave: null, handleReset: null }); ================================================ FILE: luci/applications/luci-app-smstools3/htdocs/luci-static/resources/view/smstools3/setup.js ================================================ 'use strict'; 'require form'; 'require rpc'; 'require fs'; 'require view'; 'require uci'; 'require ui'; 'require tools.widgets as widgets' var callSerialPort = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ], expect: { entries: [] }, filter: function(list, params) { var rv = []; for (var i = 0; i < list.length; i++) if (list[i].name.match(/^ttyACM/) || list[i].name.match(/^ttyUSB/) || list[i].name.match(/^wwan\d+at\d+/)) rv.push(params.path + list[i].name); return rv.sort(); } }); var callLEDs = rpc.declare({ object: 'luci', method: 'getLEDs', expect: { '': {} } }); return view.extend({ load: function() { return Promise.all([ callLEDs(), L.resolveDefault(fs.list('/www' + L.resource('view/system/led-trigger')), []), ]).then(function(data) { var plugins = data[1]; var tasks = []; for (var i = 0; i < plugins.length; i++) { var m = plugins[i].name.match(/^(.+)\.js$/); if (plugins[i].type != 'file' || m == null) continue; tasks.push(L.require('view.system.led-trigger.' + m[1]).then(L.bind(function(name){ return L.resolveDefault(L.require('view.system.led-trigger.' + name)).then(function(form) { return { name: name, form: form, }; }); }, this, m[1]))); } return Promise.all(tasks).then(function(plugins) { var value = {}; value[0] = data[0]; value[1] = plugins; return value; }); }); }, render: function(data) { var m, s, o, triggers = []; var leds = data[0]; var plugins = data[1]; for (var k in leds) for (var i = 0; i < leds[k].triggers.length; i++) triggers[i] = leds[k].triggers[i]; m = new form.Map('smstools3', _('Smstools3: Setup'), _('Configure smstools3 daemon.')); s = m.section(form.TypedSection, 'sms', null); //s.tab('general', _('General')); //s.tab('advanced', _('Advanced')); s.anonymous = true; o = s.option(form.Flag, 'decode_utf', _('Decode SMS'), _('Decode Incoming messages to UTF-8 codepage.')); o.rmempty = true; o = s.option(form.ListValue, 'storage', _('SMS Storage'), _('Select storage to save SMS.')); o.value('temporary', _('Temporary')); o.value('persistent', _('Persistent')); o.default = 'temporary'; o = s.option(form.ListValue, 'loglevel', _('Loglevel'), _('Logging output.')); o.value('1', _('Emergency')); o.value('2', _('Alert')); o.value('3', _('Critical')); o.value('4', _('Error')); o.value('5', _('Warning')); o.value('6', _('Notice')); o.value('7', _('Info')); o.value('8', _('Debug')); o.default = '5'; o = s.option(form.Flag, 'led_enable', _('LED'), _('LED indicate to Incoming messages.')); o.rmempty = true; o = s.option(form.ListValue, 'led', _('Select LED')); Object.keys(leds).sort().forEach(function(name) { o.value(name); }); o.rmempty = true; o.depends('led_enable', '1'); s = m.section(form.GridSection, 'modem', _('Modems List')); s.addremove = true; s.rmempty = true; o = s.option(form.Flag, 'ui', _('Unexepted Input'), _('Enable Unexpected input from COM port.')); o.rmempty = true; o = s.option(form.ListValue, 'device', _('Select COM port')); o.load = function(section_id) { return callSerialPort('/dev/').then(L.bind(function(devices) { this.keylist = []; this.vallist = []; for (var i = 0; i < devices.length; i++) this.value(devices[i]); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o = s.option(form.ListValue, 'init', _('Init string'), _('Initialise modem for more vendors')); o.value('huawei', _('Huawei')); o.value('intel', _('Intel XMM')); o.value('asr', _('ASR or more')); o.value('', _('Qualcomm or more')); o.default = ''; o.rmempty = true; o = s.option(form.Value, 'pin', _('PIN Code'), _('Default value: not in use.
Specifies the PIN number of the SIM card inside the modem.')); o.datatype = 'and(rangelength,(4,8),uinteger)'; o.rmempty = true; o = s.option(form.ListValue, 'net_check', _('Check network'), _('Setup network checking. Some modems incorrect test network.')); o.value('0', _('Ignore')); o.value('1', _('Always')); o.value('2', _('Before messages')); o = s.option(form.Flag, 'sig_check', _('Ignore signal level'), _('Some devices do not support Bit Error Rate')); o.rmempty = true; o = s.option(form.Flag, 'enable', _('Enable')); o.modalonly = false; o.editable = true; return m.render(); } }); ================================================ FILE: luci/applications/luci-app-smstools3/po/ru/smstools3.po ================================================ msgid "" msgstr "" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlyakov \n" msgid "Incoming" msgstr "Входящие" msgid "Outcoming" msgstr "Исходящие" msgid "Push" msgstr "Отправить" msgid "Phonebook" msgstr "Тел.Книга" msgid "Commands" msgstr "Команды" msgid "Setup" msgstr "Настройка" msgid "User Script" msgstr "Сценарий" msgid "Send Date" msgstr "Отправлено" msgid "Recv.Date" msgstr "Получено" msgid "From" msgstr "Отправитель" msgid "Smstools3: Incoming messages" msgstr "Smstools3: Входящие сообщения" msgid "Remove All SMS" msgstr "Удалить все СМС" msgid "Refresh" msgstr "Обновить" msgid "Time(sec)" msgstr "Время(сек)" msgid "To" msgstr "Получатель" msgid "Message" msgstr "Сообщение" msgid "Smstools3: Outgoing messages" msgstr "Smstools3: Исходящие сообщения" msgid "Smstools3: Commands" msgstr "Smstools3: Команды" msgid "Command List interface." msgstr "Команды управления по СМС." msgid "Phone" msgstr "Ном.телеф." msgid "Root Phone list to accept commands." msgstr "Список телефонов для управления." msgid "Phone number must be without \"+\"" msgstr "Телефный номер без \"+\"" msgid "Command List" msgstr "Список команд" msgid "SMS Command" msgstr "Команда СМС" msgid "Execute" msgstr "Выполнить" msgid "Delay" msgstr "Задержка" msgid "Delay in sec." msgstr "В секундах" msgid "Answer" msgstr "Ответ" msgid "Answer MSG" msgstr "Сообщ. в ответ" msgid "Smstools3: Phonebook" msgstr "Smstools3: Телефонная книга" msgid "Phonebook" msgstr "Тел.книга" msgid "Each line must have the following format: 'Description;Phone Number'. For user convenience, the file is saved to the location /etc/smstools3.pb." msgstr "Каждая строчка должна иметь формат: 'Описание;Телефонный номер'. Файл телефонной книги находиться в /etc/smstools3.pb." msgid "Smtools3: User Script" msgstr "Smtools3: сценарий пользователя" msgid "Edit smstools3 user script. Add user\'s actions for incoming and outcoming messages.
Is shell script for smstools3 scenario. See \
smstools3 manual page\ for more details." msgstr "Отредактируйте пользовательский сценарий smstools3. Добавляйте действия для входящих и исходящих сообщений.
Это сценарий оболочки для smstools3. Более подробную информацию смотрите \
на странице руководства smstools3\." msgid "Edit User script smstools3.
File stored in /etc/smstools3.user" msgstr "Сценарий smstools3.
Файл находиться в /etc/smstools3.user" msgid "User interface for sending SMS via smsd. If use multiplie modems, every next sending sms outgoing round robin." msgstr "Отправить СМС через smsd. При использовании нескольких модемов, каждое последующее сообщение будет отправлено через следущий модем." msgid "Smstools3: send message" msgstr "Smstools3: отправить сообщение" msgid "Phone Number" msgstr "Номер телефона" msgid "Text Message" msgstr "Текстовое сообщение" msgid "Please specify the phone number to send" msgstr "Пожадуйста введите номер телефона получателя" msgid "Message sent" msgstr "Сообщение отправлено" msgid "Clear text" msgstr "Очистить" msgid "Send message" msgstr "Отправить СМС" msgid "Smstools3: Setup" msgstr "Smstools3: Настройка" msgid "Configure smstools3 daemon." msgstr "Конфигурация smstools3." msgid "General" msgstr "Основные" msgid "Advanced" msgstr "Дополнительные" msgid "Decode SMS" msgstr "Декодировать СМС" msgid "Decode Incoming messages to UTF-8 codepage." msgstr "Декодировать входящие сообщения в UTF-8." msgid "Unexepted Input" msgstr "Нештатные команды" msgid "Enable Unexpected input from COM port." msgstr "Принимать команды, не предназначенные для службы на COM-порт." msgid "SMS Storage" msgstr "Хранилице СМС" msgid "Select storage to save SMS." msgstr "Выберите тип хранилища СМС" msgid "Temporary" msgstr "Временное" msgid "Persistent" msgstr "Постоянное" msgid "Select COM port" msgstr "COM-порт" msgid "Init string" msgstr "Иниациалиция" msgid "Initialise modem for more vendors" msgstr "Инициализировать модем для некоторых производителей" msgid "ASR or more" msgstr "ARS и подобные" msgid "Qualcomm or more" msgstr "Qualcomm и подобные" msgid "PIN Code" msgstr "ПИН-код" msgid "Default value: not in use.
Specifies the PIN number of the SIM card inside the modem." msgstr "По-умалчанию не используется.
ПИН-код симкарты модема." msgid "Loglevel" msgstr "Журнал" msgid "Logging output." msgstr "Уровень журнала." msgid "Emergency" msgstr "Чрезвычайный" msgid "Alert" msgstr "Тревожный" msgid "Critical" msgstr "Критический" msgid "Error" msgstr "Ошибка" msgid "Warning" msgstr "Предупреждение" msgid "Notice" msgstr "Уведомление" msgid "Info" msgstr "Информирование" msgid "Debug" msgstr "Отладка" msgid "Check network" msgstr "Проверять сеть" msgid "Ignore" msgstr "Никогда" msgid "Always" msgstr "Всегда" msgid "Before messages" msgstr "После сообщений" msgid "Setup network checking. Some modems incorrect test network." msgstr "Задать проверку сети. Некоторые модемы неверно определяют наличие сети." msgid "Ignore signal level" msgstr "Игнорировать уровень сигнала" msgid "Some devices do not support Bit Error Rate" msgstr "Некоторые модемы не поддерживают Bit Error Rate" msgid "LED indicate to Incoming messages." msgstr "Индикация входящих сообщений." msgid "Select LED" msgstr "Выберите LED" msgid "Any modem (default)" msgstr "Любой доступный" msgid "Are you sure you want to delete this message?" msgstr "Вы уверены, что хотите удалить это сообщение?" msgid "Action" msgstr "Действие" msgid "Delete this message" msgstr "Удалить это сообщение" ================================================ FILE: luci/applications/luci-app-smstools3/po/zh_Hans/smstools3.po ================================================ msgid "" msgstr "" "Language: zh_Hans\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Y0518 \n" msgid "Incoming" msgstr "收件箱" msgid "Outcoming" msgstr "发件箱" msgid "Push" msgstr "发送" msgid "Phonebook" msgstr "电话簿" msgid "Commands" msgstr "命令" msgid "Setup" msgstr "设置" msgid "User Script" msgstr "用户脚本" msgid "Send Date" msgstr "发送日期" msgid "Recv.Date" msgstr "接收日期" msgid "From" msgstr "发件人" msgid "Smstools3: Incoming messages" msgstr "Smstools3: 接收的消息" msgid "Remove SMS" msgstr "删除短信" msgid "Refresh" msgstr "刷新" msgid "Time(sec)" msgstr "时间(秒)" msgid "To" msgstr "收件人" msgid "Message" msgstr "消息" msgid "Smstools3: Outgoing messages" msgstr "Smstools3: 发送的消息" msgid "Smstools3: Commands" msgstr "Smstools3: 命令" msgid "Command List interface." msgstr "通过短信命令的控制界面" msgid "Phone" msgstr "电话号码" msgid "Root Phone list to accept commands." msgstr "接受命令的根电话号码列表" msgid "Phone number must be without \"+\"" msgstr "电话号码必须不包含 \"+\"" msgid "Command List" msgstr "命令列表" msgid "SMS Command" msgstr "短信命令" msgid "Execute" msgstr "执行" msgid "Delay" msgstr "延迟" msgid "Delay in sec." msgstr "延迟时间(秒)" msgid "Answer" msgstr "回复" msgid "Answer MSG" msgstr "回复消息" msgid "Smstools3: Phonebook" msgstr "Smstools3: 电话簿" msgid "Phonebook" msgstr "电话簿" msgid "Each line must have the following format: 'Description;Phone Number'. For user convenience, the file is saved to the location /etc/smstools3.pb." msgstr "每行应按照以下格式:'描述;电话号码'。为了方便用户,该文件将保存在/etc/smstools3.pb位置。" msgid "Smtools3: User Script" msgstr "Smtools3: 用户脚本" msgid "Edit smstools3 user script. Add user's actions for incoming and outcoming messages.
Is shell script for smstools3 scenario. See
smstools3 manual page for more details." msgstr "编辑smstools3的用户脚本。为接收到和发送的消息添加用户的处理动作。
这是用于smstools3场景的Shell脚本。欲了解更多信息,请参阅smstools3手册页面。" msgid "Edit User script smstools3.
File stored in /etc/smstools3.user" msgstr "编辑Smstools3用户脚本。
文件存储在 /etc/smstools3.user" msgid "User interface for sending SMS via smsd." msgstr "通过smsd发送短信的用户界面。" msgid "Smstools3: send message" msgstr "Smstools3:发送消息" msgid "Phone Number" msgstr "电话号码" msgid "Text Message" msgstr "文本消息" msgid "Please specify the phone number to send" msgstr "请输入接收短信的电话号码" msgid "Message sent" msgstr "消息已发送" msgid "Clear text" msgstr "清除文本" msgid "Send message" msgstr "发送短信" msgid "Smstools3: Setup" msgstr "Smstools3:设置" msgid "Configure smstools3 daemon." msgstr "配置smstools3守护进程。" msgid "General" msgstr "常规" msgid "Advanced" msgstr "高级" msgid "Decode SMS" msgstr "解码短信" msgid "Decode Incoming messages to UTF-8 codepage." msgstr "将接收到的消息解码为UTF-8编码。" msgid "Unexepted Input" msgstr "未预期的输入" msgid "Enable Unexpected input from COM port." msgstr "允许COM端口接收非预设服务的命令。" msgid "SMS Storage" msgstr "短信存储" msgid "Select storage to save SMS." msgstr "选择用于保存短信的存储方式。" msgid "Temporary" msgstr "临时" msgid "Persistent" msgstr "持久化" msgid "Select COM port" msgstr "选择COM端口" msgid "Init string" msgstr "初始化串行指令" msgid "Initialise modem for more vendors" msgstr "针对更多供应商初始化调制解调器" msgid "ASR or more" msgstr "ASR或其他类似设备" msgid "Qualcomm or more" msgstr "Qualcomm或其他类似设备" msgid "PIN Code" msgstr "PIN码" msgid "Default value: not in use.
Specifies the PIN number of the SIM card inside the modem." msgstr "默认值:未使用。
指定内置在调制解调器中的SIM卡的PIN码。" msgid "Loglevel" msgstr "日志级别" msgid "Logging output." msgstr "记录输出等级。" msgid "Emergency" msgstr "紧急情况" msgid "Alert" msgstr "警报" msgid "Critical" msgstr "严重" msgid "Error" msgstr "错误" msgid "Warning" msgstr "警告" msgid "Notice" msgstr "通知" msgid "Info" msgstr "信息" msgid "Debug" msgstr "调试" msgid "Check network" msgstr "检查网络" msgid "Ignore" msgstr "忽略" msgid "Always" msgstr "始终" msgid "Before messages" msgstr "在消息之前" msgid "Setup network checking. Some modems incorrect test network." msgstr "设置网络检测。某些调制解调器可能无法正确检测网络状况。" msgid "Ignore signal level" msgstr "忽略信号强度" msgid "Some devices do not support Bit Error Rate" msgstr "某些设备不支持误码率(Bit Error Rate)" msgid "LED indicate to Incoming messages." msgstr "使用LED指示灯显示接收到的短信。" msgid "Select LED" msgstr "选择LED指示灯" ================================================ FILE: luci/applications/luci-app-smstools3/root/etc/init.d/luci-sms ================================================ #!/bin/sh /etc/rc.common START=99 USE_PROCD=1 start_service(){ sleep 10 && \ /usr/share/luci-app-smstools3/event.sh > /tmp/smsd.conf && \ rm -f /etc/smsd.conf && ln -s /tmp/smsd.conf /etc && \ /etc/init.d/smstools3 stop && \ sleep 3 && \ /etc/init.d/smstools3 start && \ /etc/init.d/led restart & } reload_service(){ start } service_triggers() { procd_add_reload_trigger smstools3 } ================================================ FILE: luci/applications/luci-app-smstools3/root/etc/uci-defaults/63_luci-app-smstools3 ================================================ #!/bin/sh if [ ! -f /etc/config/smstools3 ]; then touch /etc/config/smstools3 touch /etc/smstools3.user touch /etc/smstools3.pb uci add smstools3 sms uci set smstools3.@sms[-1].decode_utf='1' uci add smstools3 root_phone uci commit smstools3 fi ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/bin/msg_control ================================================ #!/bin/sh IFS=$'\n' function send_msg(){ local filename=$(basename "$2") modem=$(echo "$1" | awk '/Modem:/{print $2}') to=$(echo "$1" | awk '/To:/{print $2}') sent=$(echo "$1" | awk '/Sent:/{gsub("Sent: ","");print $0}') time=$(echo "$1" | awk '/Sending_time:/{print $2}') msg=$(echo "$1" |sed -e '1,/^$/ d' -e 's/[]["]/\\&/g') # jq processing modem_json=$(jq -Rn --arg str "$modem" '$str') msg_json=$(jq -Rn --arg str "$msg" '$str') string="{ \"filename\": \"$filename\", \"modem\": $modem_json, \"to\": \"$to\", \"sent\": \"$sent\", \"time\": \"$time\", \"content\": $msg_json }," } function recv_msg(){ local filename=$(basename "$2") local modem from srecv drecv msg modem=$(echo "$1" | awk '/Modem:/{print $2}') from=$(echo "$1" | awk '/From:/{print $2}') srecv=$(echo "$1" | awk '/Sent:/{gsub("Sent: ","");print $0}') drecv=$(echo "$1" | awk '/Received:/{gsub("Received: ","");print $0}') msg=$(echo "$1" |sed -e '1,/^$/ d' -e 's/[]["]/\\&/g' -e 's/[\r\n\^M]//g' | awk '{printf "%s
",$0} END {print ""}') # jq processing modem_json=$(jq -Rn --arg str "$modem" '$str') from_json=$(jq -Rn --arg str "$from" '$str') srecv_json=$(jq -Rn --arg str "$srecv" '$str') drecv_json=$(jq -Rn --arg str "$drecv" '$str') msg_json=$(jq -Rn --arg str "$msg" '$str') string="{ \"filename\": \"$filename\", \"modem\": $modem_json, \"from\": $from_json, \"srecv\": $srecv_json, \"drecv\": $drecv_json, \"content\": $msg_json }," } case $1 in sent) MSG=$(find /var/spool/sms/sent/ -type f | sort -r) echo "{ \"sent\": [" for m in $MSG; do f=$(cat $m) send_msg "$f" "$m" json="$json $string" done echo -e $json | sed 's/.$//' echo " ]}" ;; recv) MSG=$(find /var/spool/sms/incoming/ -type f | sort -r) echo "{ \"recv\": [" for m in $MSG; do f=$(cat $m) recv_msg "$f" "$m" json="$json $string" done echo -e $json | sed 's/.$//' echo " ]}" ;; rmsent) rm -rf /var/spool/sms/sent/* ;; rmrecv) rm -rf /var/spool/sms/incoming/* ;; delete) if [ -n "$2" ]; then if [ -f "/var/spool/sms/incoming/$2" ]; then rm -f "/var/spool/sms/incoming/$2" elif [ -f "/var/spool/sms/sent/$2" ]; then rm -f "/var/spool/sms/sent/$2" fi fi ;; esac ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/bin/send_sms ================================================ #!/bin/ash # This script send a text sms at the command line by creating # a sms file in the outgoing queue. # $1 is the destination phone number. # $2 is the message text. # $3 is the modem name (optional) - from smsd.conf section names # If you leave $2 or both empty, the script will ask you. # If you give more than 2 arguments, last is taken as a text and # all other are taken as destination numbers. # If a destination is asked, you can type multiple numbers # delimited with spaces. # Keys for example: "password" and "keke": # KEYS="5f4dcc3b5aa765d61d8327deb882cf99 4a5ea11b030ec1cfbc8b9947fdf2c872 " KEYS="" # When creating keys, remember to use -n for echo: # echo -n "key" | md5sum smsd_group="smsd" SMSD_CONF="/etc/smsd.conf" # Will need echo which accepts -n argument: ECHO=echo case `uname` in SunOS) ECHO=/usr/ucb/echo ;; esac # get lst modems from smsd.conf get_modems_from_conf() { if [ -f "$SMSD_CONF" ]; then grep -E '^\[.*\]$' "$SMSD_CONF" | sed 's/\[\(.*\)\]/\1/' | grep -v '^global$' | tr '\n' ' ' else echo "" fi } modem_exists() { local modem=$1 if [ -f "$SMSD_CONF" ]; then grep -q "^\[$modem\]$" "$SMSD_CONF" return $? else return 1 fi } get_first_modem() { local modems=$(get_modems_from_conf) if [ -n "$modems" ]; then echo "$modems" | awk '{print $1}' else echo "" fi } check_modem_availability() { local modem=$1 if ps | grep -v grep | grep -q smsd; then echo "Using modem: $modem" >&2 return 0 else local device=$(grep -A 10 "^\[$modem\]" "$SMSD_CONF" 2>/dev/null | grep "device\s*=" | head -1 | awk -F= '{print $2}' | tr -d ' ') if [ -n "$device" ] && [ -e "$device" ]; then return 0 else return 1 fi fi } if ! [ -z "$KEYS" ]; then printf "Key: " read KEY if [ -z "$KEY" ]; then echo "Key required, stopping." exit 1 fi KEY=`$ECHO -n "$KEY" | md5sum | awk '{print $1;}'` if ! echo "$KEYS" | grep "$KEY" >/dev/null; then echo "Incorrect key, stopping." exit 1 fi fi DEST=$1 TEXT=$2 MODEM_NAME=$3 case $# in 0) printf "Destination(s): " read DEST if [ -z "$DEST" ]; then echo "No destination, stopping." exit 1 fi printf "Text: " read TEXT if [ -z "$TEXT" ]; then echo "No text, stopping." exit 1 fi ;; 1) printf "Text: " read TEXT if [ -z "$TEXT" ]; then echo "No text, stopping." exit 1 fi ;; 2) destinations=$DEST ;; 3) destinations=$DEST TEXT=$2 MODEM_NAME=$3 ;; *) n=$# while [ $n -gt 1 ]; do if [ $n -eq 2 ]; then if modem_exists "$1"; then MODEM_NAME=$1 else if [ -z "$TEXT" ]; then TEXT=$1 else destinations="$destinations $1" fi fi else destinations="$destinations $1" fi shift n=`expr $n - 1` done if [ -z "$TEXT" ]; then TEXT=$1 fi ;; esac if [ -z "$destinations" ] && [ -n "$DEST" ]; then destinations=$DEST fi AUTO_SELECTED=false if [ -z "$MODEM_NAME" ]; then MODEM_NAME=$(get_first_modem) if [ -n "$MODEM_NAME" ]; then AUTO_SELECTED=true echo "No modem specified, auto-selected: $MODEM_NAME" else echo "Error: No modems found in configuration and no modem specified." exit 1 fi else if ! modem_exists "$MODEM_NAME"; then echo "Warning: Modem '$MODEM_NAME' not found in config." MODEM_NAME=$(get_first_modem) if [ -n "$MODEM_NAME" ]; then AUTO_SELECTED=true echo "Auto-selected available modem: $MODEM_NAME" else echo "Error: No modems available in configuration." exit 1 fi fi fi if ! check_modem_availability "$MODEM_NAME"; then echo "Warning: Modem '$MODEM_NAME' might not be available." ALL_MODEMS=$(get_modems_from_conf) for alt_modem in $ALL_MODEMS; do if [ "$alt_modem" != "$MODEM_NAME" ] && check_modem_availability "$alt_modem"; then echo "Switching to alternative modem: $alt_modem" MODEM_NAME=$alt_modem AUTO_SELECTED=true break fi done fi echo "-- " echo "Text: $TEXT" echo "Modem: $MODEM_NAME" [ "$AUTO_SELECTED" = "true" ] && echo "Note: Modem was auto-selected" ALPHABET="" if which iconv > /dev/null 2>&1; then if ! $ECHO -n "$TEXT" | iconv -t ISO-8859-15 >/dev/null 2>&1; then ALPHABET="Alphabet: UCS" fi fi group="" if [ -f /etc/group ]; then if grep $smsd_group: /etc/group >/dev/null; then group=$smsd_group fi fi for destination in $destinations; do echo "To: $destination" TMPFILE=`mktemp /tmp/smsd_XXXXXX` $ECHO "To: $destination" >> $TMPFILE [ -n "$ALPHABET" ] && $ECHO "$ALPHABET" >> $TMPFILE [ -n "$MODEM_NAME" ] && $ECHO "Modem: $MODEM_NAME" >> $TMPFILE $ECHO "" >> $TMPFILE if [ -z "$ALPHABET" ]; then $ECHO -n "$TEXT" >> $TMPFILE else $ECHO -n "$TEXT" | iconv -t UNICODEBIG >> $TMPFILE fi if [ "x$group" != x ]; then chgrp $group $TMPFILE fi chmod 0660 $TMPFILE FILE=`mktemp /var/spool/sms/outgoing/send_XXXXXX` mv $TMPFILE $FILE echo "Message queued: $FILE" done echo "SMS will be sent via modem: $MODEM_NAME" ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/share/luci/menu.d/luci-app-smstools3.json ================================================ { "admin/modem": { "title": "Modem", "order": 45, "action": { "type": "firstchild", "recurse": true } }, "admin/modem/sms": { "title": "Smstools3 SMS", "order": 21, "action": { "type": "alias", "path": "admin/modem/sms/in" }, "depends": { "uci": { "smstools3": true}, "acl": [ "luci-app-smstools3" ] } }, "admin/modem/sms/in": { "title": "Incoming", "order": 22, "action": { "type": "view", "path": "smstools3/in" } }, "admin/modem/sms/out": { "title": "Outcoming", "order": 23, "action": { "type": "view", "path": "smstools3/out" } }, "admin/modem/sms/send": { "title": "Push", "order": 24, "action": { "type": "view", "path": "smstools3/send" } }, "admin/modem/sms/pb": { "title": "Phonebook", "order": 25, "action": { "type": "view", "path": "smstools3/pb" } }, "admin/modem/sms/command": { "title": "Commands", "order": 26, "action": { "type": "view", "path": "smstools3/cmd" } }, "admin/modem/sms/setup": { "title": "Setup", "order": 27, "action": { "type": "view", "path": "smstools3/setup" } }, "admin/modem/sms/script": { "title": "User Script", "order": 28, "action": { "type": "view", "path": "smstools3/script" } } } ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/share/luci-app-smstools3/event.sh ================================================ #!/bin/sh killall smsd . /lib/functions.sh # Modems in system config MODEMS="" config_load smstools3 get_modem_names() { local modem_name="$1" config_get ENABLE "$modem_name" enable [ "$ENABLE" = "1" ] && MODEMS="${MODEMS}${MODEMS:+, }$modem_name" } config_foreach get_modem_names modem # General settings using config_get DECODE=$(uci -q get smstools3.@sms[0].decode_utf) STORAGE=$(uci -q get smstools3.@sms[0].storage) LOG=$(uci -q get smstools3.@sms[0].loglevel) LED_EN=$(uci -q get smstools3.@sms[0].led_enable) # Set default loglevel if not set [ -z "$LOG" ] && LOG="5" if [ ! -d /root/sms ]; then mkdir -p /root/sms for d in checked failed incoming outgoing sent; do mkdir -p /root/sms/${d} done fi case "$STORAGE" in persistent) if [ -d /var/spool/sms ]; then mv /var/spool/sms /var/spool/sms_tmp ln -sf /root/sms /var/spool/sms fi ;; temporary) if [ -d /var/spool/sms_tmp ]; then rm -f /var/spool/sms mv /var/spool/sms_tmp /var/spool/sms fi ;; esac # template config echo "devices = $MODEMS" echo "incoming = /var/spool/sms/incoming" echo "outgoing = /var/spool/sms/outgoing" echo "checked = /var/spool/sms/checked" echo "failed = /var/spool/sms/failed" echo "sent = /var/spool/sms/sent" echo "receive_before_send = no" echo "date_filename = 1" echo "date_filename_format = %s" echo "eventhandler = /usr/share/luci-app-smstools3/led.sh" [ -n "$DECODE" ] && { echo "decode_unicode_text = yes" echo "incoming_utf8 = yes" } echo "receive_before_send = no" echo "autosplit = 4" [ -n "$LOG" ] && echo "loglevel = $LOG" # Process each modem using config_foreach process_modem() { local modem_name="$1" local UI DEVICE PIN INIT_ NET_CHECK SIG_CHECK ENABLE config_get UI "$modem_name" ui config_get DEVICE "$modem_name" device config_get PIN "$modem_name" pin config_get INIT_ "$modem_name" init config_get NET_CHECK "$modem_name" net_check config_get SIG_CHECK "$modem_name" sig_check config_get ENABLE "$modem_name" enable [ "$ENABLE" = "1" ] || return 0 echo "" echo "[${modem_name}]" case "$INIT_" in huawei) echo "init = AT+CPMS=\"SM\";+CNMI=2,0,0,2,1" ;; intel) echo "init = AT+CPMS=\"SM\"" ;; asr) echo "init = AT+CPMS=\"SM\",\"SM\",\"SM\"" ;; *) echo "init = AT+CPMS=\"ME\",\"ME\",\"ME\"" ;; esac echo "device = $DEVICE" case "$SIG_CHECK" in 1) echo "signal_quality_ber_ignore = yes" ;; esac case "$NET_CHECK" in 0) echo "check_network = 0" ;; 1) echo "check_network = 1" ;; 2) echo "check_network = 2" ;; esac [ -z "$UI" ] && echo "detect_unexpected_input = no" echo "incoming = yes" # PIN validation [ -n "$PIN" ] && { case "${PIN#}" in *[!0-9]*) logger -t luci-app-smstools3 "invalid pin for modem $modem_name" ;; *[0-9]*) [ ${#PIN} -lt 4 -a ${#PIN} -gt 8 ] && { echo "pin = $PIN" } || { logger -t luci-app-smstools3 "invalid pin length for modem $modem_name" } ;; esac } echo "baudrate = 115200" } # Process all modems config_foreach process_modem modem ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/share/luci-app-smstools3/led.sh ================================================ #!/bin/sh LED_EN=$(uci -q get smstools3.@sms[0].led_enable) LED=$(uci -q get smstools3.@sms[0].led) case $1 in off) if [ $LED_EN ]; then echo none > /sys/class/leds/${LED}/trigger /etc/init.d/led restart fi ;; RECEIVED) if [ $LED_EN ]; then echo timer > /sys/class/leds/${LED}/trigger fi ;; esac if [ -r /usr/share/luci-app-smstools3/smscommand.sh ]; then . /usr/share/luci-app-smstools3/smscommand.sh fi if [ -r /etc/smstools3.user ]; then . /etc/smstools3.user fi NUM=$(ls -1 /var/spool/sms/incoming/ | wc -l) BODY=$(echo $2 | awk -F [\/] '{print $NF}') if [ $NUM -ge 100 ] && [ $NUM -lt 1000 ]; then NUM=0$NUM elif [ $NUM -ge 10 ] && [ $NUM -lt 100 ]; then NUM=00$NUM elif [ $NUM -ge 0 ] && [ $NUM -lt 10 ]; then NUM=000$NUM fi case $1 in RECEIVED) mv $2 /var/spool/sms/incoming/${NUM}_${BODY} rm -f /var/spool/sms/incoming/*concat* ;; SENT) if sed -e '/^$/ q' < "$2" | grep "^Alphabet: UCS" > /dev/null; then TMPFILE=`mktemp /tmp/smsd_XXXXXX` sed -e '/^$/ q' < "$2" | sed -e 's/Alphabet: UCS/Alphabet: UTF-8/g' > $TMPFILE sed -e '1,/^$/ d' < $2 | iconv -f UNICODEBIG -t UTF-8 >> $TMPFILE mv -f $TMPFILE $2 fi ;; esac ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/share/luci-app-smstools3/smscommand.sh ================================================ #!/bin/sh # luci-app-smstools3 command handler by koshev-msk 2025 #SECTIONS=$(uci show smstools3 | awk -F [\.][\]\[\@=] '/=command/{print $3}') SECTIONS=$(uci show smstools3 | awk -F [\.,=] '/=command/{print $2}') PHONE=$(uci -q get smstools3.@root_phone[0].phone) # Send SMS send_sms() { local phone="$1" local message="$2" local modem="$3" if [ -n "$modem" ] && [ "$modem" != "" ]; then # if modem selected /usr/bin/send_sms "$phone" "$message" "$modem" else # if not /usr/bin/send_sms "$phone" "$message" fi } # smscommand function smscmd(){ local message_modem="$1" for s in $SECTIONS; do CMD="$(uci -q get smstools3.${s}.command)" MSG="$(echo $content)" COMMAND_MODEM=$(uci -q get smstools3.${s}.modem) # check recieved case $MSG in *${CMD}*) # check modem if [ -n "$COMMAND_MODEM" ] && [ "$COMMAND_MODEM" != "" ]; then if [ "$message_modem" != "$COMMAND_MODEM" ]; then # if not from modem message continue fi fi # run commmand ANSWER=$(uci -q get smstools3.${s}.answer) if [ "$ANSWER" ]; then send_sms "$PHONE" "$ANSWER" "$COMMAND_MODEM" fi EXEC=$(uci -q get smstools3.${s}.exec) DELAY=$(uci -q get smstools3.${s}.delay) if [ $DELAY ]; then sleep $DELAY && $EXEC & else $EXEC fi ;; esac done } # parse incoming message if [ "$1" == "RECEIVED" ]; then from=`grep "From:" $2 | awk -F ': ' '{printf $2}'` content=$(sed -e '1,/^$/ d' < "$2") message_modem=`grep "Modem:" $2 | awk -F ': ' '{printf $2}'` # check ROOT messages for n in ${PHONE}; do if [ "$from" -eq "$n" ]; then PHONE=$n smscmd "$message_modem" fi done fi ================================================ FILE: luci/applications/luci-app-smstools3/root/usr/share/luci-app-smstools3/smstools3.user ================================================ #!/bin/sh # Simple resend incoming messages to telegram ROUTER=$(uci -q get system.@system[0].hostname) chat_id=$(uci -q get telegrambot.config.chat_id) token=$(uci -q get telegrambot.config.bot_token) # parse incoming message if [ "$1" == "RECEIVED" ]; then from=`grep "From:" $2 | awk -F ': ' '{printf $2}'` content=$(sed -e '1,/^$/ d' < "$2") text=$(cat < # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Luci app for ssw LUCI_DEPENDS:= +luci-app-modeminfo PKG_LICENSE:=GPLv3 PKG_VERSION:=0.0.1 PKG_RELEASE:=1 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-ssw/htdocs/luci-static/resources/view/modem/ssw.js ================================================ 'use strict'; 'require form'; 'require fs'; 'require ui'; 'require uci'; 'require view'; 'require poll'; 'require dom'; return view.extend({ render: function(modems) { var m, s, o m = new form.Map('ssw', _('SSW - SIM Card switch')) s = m.section(form.TypedSection, 'modem'); o = s.option(form.ListValue, 'value', _('State')); o.value(1, _('Enable')); o.value(0, _('Disable')); s = m.section(form.TypedSection, 'sim'); o = s.option(form.ListValue, 'value', _('Default SIM Slot')); o.value(1, _('SLOT 1')); o.value(0, _('SLOT 2')); s = m.section(form.TypedSection, 'failover'); o = s.option(form.Flag, 'enable', _('Enable')); o = s.option(form.Value, 'apn1', _('APN Default SIM')); o.depends({enable: '1'}); o = s.option(form.Value, 'apn2', _('APN Reserved SIM')); o.depends({enable: '1'}); o = s.option(form.Flag, 'revert', _('Revert'), _('Revert to default sim slot. Each failed attempt doubles revert time.')); o.depends({enable: '1'}); o = s.option(form.ListValue, 'rsrp', _('RSRP value'), _('Switch sim lower by value.')); for (var rsrp = -120; rsrp <= -80; rsrp++) { o.value(rsrp, rsrp +' '+ _('dBm')); }; o.depends({enable: '1'}); o = s.option(form.Value, 'interval', _('Interval check. sec')); o.value('5', 5 +' '+ _('sec')); for (var sec = 10; sec <= 60; sec+=10) { o.value(sec,sec +' '+ _('sec')); }; o.value('2m', 2 +' '+ _('minute')); o.value('5m', 5 +' '+ _('minute')); for (var d = 10; d <= 60; d+=10) { o.value(d+'m',d +' '+ _('minute')); }; o.value('2h', 2 +' '+ _('hour')); o.value('4h', 4 +' '+ _('hour')); o.depends({enable: '1'}); o = s.option(form.Value, 'times_rsrp', _('Probes'), _('RSRP check average values.
NOTE: all time check is Interval*Probes
Example: Interval=60 sec, Probes=5 times, all time check 300 sec.')); for (var p = 5; p <=10; p++) { o.value(p,p); }; o.depends({enable: '1'}); return m.render(); } }); ================================================ FILE: luci/applications/luci-app-ssw/po/ru/ssw.po ================================================ msgid "SSW - SIM Card switch" msgstr "SSW - управление SIM картами" msgid "Enable" msgstr "Включить" msgid "Disable" msgstr "Отключить" msgid "Default SIM Slot" msgstr "SIM cлот по-умолчанию" msgid "APN Default SIM" msgstr "APN SIM по-умолчанию" msgid "APN Reserved SIM" msgstr "APN резервной SIM" msgid "Revert" msgstr "Вернуть" msgid "Revert to default sim slot. Each failed attempt doubles revert time." msgstr "Вернуть на SIM слот по-умолчанию. Каждая неудачная попытка увеличивает время возврата." msgid "RSRP value" msgstr "Значение RSRP" msgid "Switch sim lower by value." msgstr "Переключить sim ниже значения." msgid "Interval check. sec" msgstr "Интервал проверки. сек" msgid "Probes" msgstr "Попытки" msgid "RSRP check average values.
NOTE: all time check is Interval*Probes
Example: Interval=60 sec, Probes=5 times, all time check 300 sec." msgstr "Проверяется усреднённое значение RSRP.
ВНИМАНИЕ: общее время итоговой проверки равно Интервал*Попытки
Например: интервал=60 sec, попыток=5, время до переключения 300 сек." ================================================ FILE: luci/applications/luci-app-ssw/root/usr/share/luci/menu.d/luci-app-ssw.json ================================================ { "admin/modem/main": { "title": "Modeminfo", "order": 10, "action": { "type": "alias", "path": "admin/modem/main/main" }, "depends": { "acl": [ "luci-app-ssw" ], "uci": { "system": true } } }, "admin/modem/main/ssw": { "title": "SIM", "order": 73, "action": { "type": "view", "path": "modem/ssw" } } } ================================================ FILE: luci/applications/luci-app-ssw/root/usr/share/rpcd/acl.d/luci-app-ssw.json ================================================ { "luci-app-ssw": { "description": "Grant access to ssw configuration", "read": { "file": { "/etc/init.d/ssw": [ "exec" ] }, "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "changes", "get" ] }, "uci": [ "ssw" ] }, "write": { "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "add", "apply", "confirm", "delete", "order", "rename", "set" ] }, "uci": [ "ssw" ] } } } ================================================ FILE: luci/applications/luci-app-telegrambot/Makefile ================================================ include $(TOPDIR)/rules.mk LUCI_TITLE:=TelegramBot simple webUI LUCI_DEPENDS:=+telegrambot PKG_LICENSE:=GPLv3 PKG_VERSION:=0.1.0 PKG_RELEASE:=3 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-telegrambot/README.md ================================================ # luci-app-telegrambot ================================================ FILE: luci/applications/luci-app-telegrambot/htdocs/luci-static/resources/view/telegrambot.js ================================================ 'use strict'; 'require form'; 'require fs'; 'require view'; 'require uci'; 'require ui'; 'require tools.widgets as widgets' return view.extend({ render: function(data) { var m, s, o m = new form.Map('telegrambot', _('TelegramBot'), _('Telegram bot for router with firmware Lede/Openwrt.')); s = m.section(form.TypedSection, 'telegram_bot', null); s.anonymous = true; o = s.option(form.Flag, 'enabled', _('Enable'), _('Enable Bot')); o.rmempty = true; o = s.option(form.Value, 'bot_token', _('Bot Token'), _('Token ID your Telegram Bot')); o.password = true; o = s.option(form.Value, 'chat_id', _('Chat ID'), _('Chat ID your Telegram')); o = s.option(form.Value, 'timeout', _('Delay'), _('Time Out respone Bot in sec.')); o = s.option(form.Value, 'polling_time', _('Polling Time'), _('Polling Time in sec.')); o = s.option(form.Value, 'plugins', _('Plugins'), _('Path to plugins directory.')); o.default = ('/usr/lib/telegrambot/plugins'); o = s.option(form.Value, 'log_file', _('Log File'), _('Log File')); o.default = ('/tmp/telegrambot.log'); return m.render(); } }); ================================================ FILE: luci/applications/luci-app-telegrambot/po/ru/telegrambot.po ================================================ "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Konstantine Shevlyakov \n" msgid "TelegramBot" msgstr "ТелеграмБот" msgid "Telegram bot for router with firmware Lede/Openwrt." msgstr "Телеграм бот для ротеров на прошивке Lede/Openwrt." msgid "Enable" msgstr "Включить" msgid "Enable Bot" msgstr "Включить бота" msgid "Bot Token" msgstr "Токен бота" msgid "Token ID your Telegram Bot" msgstr "Токен ID Вашего бота в Telegram" msgid "Chat ID" msgstr "ID чата" msgid "Chat ID your Telegram" msgstr "ID чата Вашего Telegram" msgid "Delay" msgstr "Задержка" msgid "Time Out respone Bot in sec." msgstr "Задержка ответа бота в сек." msgid "Polling Time" msgstr "Время опроса" msgid "Polling Time in sec." msgstr "Врема опроса в сек." msgid "Plugins" msgstr "Плагины" msgid "Path to plugins directory." msgstr "Путь к директории с плагинами." msgid "Log File" msgstr "Файл лога" ================================================ FILE: luci/applications/luci-app-telegrambot/root/usr/share/luci/menu.d/luci-app-telegrambot.json ================================================ { "admin/services/telegrambot": { "title": "TelegramBot", "order": 63, "action": { "type": "view", "path": "telegrambot" }, "depends": { "acl": [ "luci-app-telegrambot" ], "uci": { "telegrambot": true } } } } ================================================ FILE: luci/applications/luci-app-telegrambot/root/usr/share/rpcd/acl.d/luci-app-telegrambot.json ================================================ { "luci-app-telegrambot": { "description": "Grant UCI access for luci-app-telegrambot", "read": { "uci": [ "telegrambot" ] }, "write": { "uci": [ "telegrambot" ] } } } ================================================ FILE: luci/applications/luci-app-ttl/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Antitethering module for luci-app-firewall #LUCI_DEPENDS:= +luci-app-firewall PKG_LICENSE:=Apache-2.0 PKG_VERSION:=0.0.5 PKG_RELEASE:=3 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/applications/luci-app-ttl/htdocs/luci-static/resources/view/firewall/ttl.js ================================================ 'use strict'; 'require view'; 'require ui'; 'require rpc'; 'require uci'; 'require form'; 'require fs'; 'require network'; 'require firewall as fwmodel'; 'require tools.firewall as fwtool'; 'require tools.widgets as widgets'; var briefInfo = _('In Method proxy Proxy server must be configured in transparent mode.
Default on lan ipaddress interface and port 3128 tcp.
Disable masquerade recommended.'); return view.extend({ render: function() { var m, s, o; m = new form.Map('ttl', _('Antitethering Config'), briefInfo); s = m.section(form.TypedSection, 'ttl'); s.anonymous = true; s = m.section(form.TypedSection, 'ttl', _('TTL or Proxy')); s.anonymous = true; s.addremove = true; o = s.option(widgets.NetworkSelect, 'iface', _('Set interface')); o.exclude = s.section; o.nocreate = true; o.optional = true; o = s.option(form.ListValue, 'method', _('Method'), _('TTL method outgoing interface
Proxy method incoming interface')); o.value('ttl', 'TTL'); o.value('proxy', 'Proxy'); o = s.option(form.Flag, 'advanced', _('Advanced Option')); o.default = '0'; o.rmempty = false; o = s.option(form.ListValue, 'inet', _('Inet Family')); o.value('ipv4', 'IPv4'); o.value('ipv6', 'IPv6'); o.value('ipv4v6', _('Both')); o.rmempty = true; o.editable = true; o.depends('advanced', '1'); o = s.option(form.Value, 'ttl', _('TTL Value'), _('Select TTL value. Range 1 - 255')); o.value('64','64') o.value('128','128') o.default = '64'; o.rmempty = true; o.editable = true; o.depends({advanced: '1', method: /ttl/}); o = s.option(form.Value, 'ports', _('Ports'), _('Incoming ports route to proxy-server
Custom ports range: 0-65535')); o.editable = true; o.rmempty = true; o.value('all', _('ALL Ports')); o.value('http', _('HTTP Ports')); o.default = 'all'; o.depends({advanced: '1', method: /proxy/}) o = s.option(form.Value, 'proxy', _('Proxy Server Address'), _('IP-address proxy
Format: ipaddress:port
If not defined: use selected interface address.
Default: lan interface ipaddress')); o.datatype = 'ipaddrport'; o.depends({advanced: '1', method: /proxy/}) return m.render(); }, }); ================================================ FILE: luci/applications/luci-app-ttl/po/ru/ttl.po ================================================ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" "Language: ru\n" "Last-Translator: Konstantine Shevlakov Default on lan ipaddress interface and port 3128 tcp.
Disable masquerade recommended.'" msgstr "В режиме прокси, прокси-сервер должен быть настроен в прозрачном режиме.
По-умолчанию прокси-сервер ожидается на адресе lan-интерфейса на порту 3128 tcp.
Рекомендуется отключить NAT." msgid "Antitethering Config" msgstr "Настройки антитетеринга" msgid "TTL or Proxy antitether" msgstr "TTL или прокси" msgid "Set interface" msgstr "Выбор интерфейса" msgid "Method" msgstr "Способ" msgid "TTL method outgoing interface
Proxy method incoming interface" msgstr "Для метода TTL интерфейс - исходящий
Для метода прокси интерфейс - входящий" msgid "Inet Family" msgstr "Протокол IP" msgid "Advanced Option" msgstr "Дополнительно" msgid "Both" msgstr "Оба" msgid "Ports" msgstr "Порты" msgid "TTL Value" msgstr "Значение TTL" msgid "Select TTL value. Range 1 - 255" msgstr "Выбор значения TTL. От 1 до 255" msgid "Incoming ports route to proxy-server
Custom ports range: 0-65535" msgstr "Порты для перенаправления на прокси-сервер
Диапазон 0-65535" msgid "ALL Ports" msgstr "Все порты" msgid "HTTP Ports" msgstr "Порты HTTP" msgid "Proxy Server Address" msgstr "Адрес прокси-сервера" msgid "IP-address proxy
Format: ipaddress:port
If not defined: use selected interface address.
Default: lan interface ipaddress" msgstr "IP-адрес прокси
Формат: ip-адрес:порт
Если не задано: используется адрес выбранного интерфейса.
По-умолчанию: адрес интерфейса lan" ================================================ FILE: luci/applications/luci-app-ttl/root/etc/config/ttl ================================================ ================================================ FILE: luci/applications/luci-app-ttl/root/etc/hotplug.d/iface/90-ttl ================================================ . /lib/functions.sh handle_config(){ config_get iface "$1" iface [ "$iface" = "$INTERFACE" ] && { run_ttl=1 } } config_load ttl config_foreach handle_config ttl case $run_ttl in 1) /etc/init.d/ttl reload ;; esac exit 0 ================================================ FILE: luci/applications/luci-app-ttl/root/etc/init.d/ttl ================================================ #!/bin/sh /etc/rc.common START=96 USE_PROCD=1 start_service(){ /usr/share/ttl.sh logger -t "TTL" "TTL reload" } reload_service(){ sleep 10 && start & } service_triggers() { procd_add_reload_trigger ttl } ================================================ FILE: luci/applications/luci-app-ttl/root/usr/share/luci/menu.d/luci-app-ttl.json ================================================ { "admin/network/firewall": { "title": "Firewall", "order": 60, "action": { "type": "alias", "path": "admin/network/firewall/zones" }, "depends": { "acl": [ "luci-app-firewall" ], "fs": { "/sbin/fw3": "executable" }, "uci": { "firewall": true } } }, "admin/network/firewall/ttl": { "title": "TTL", "order": 45, "action": { "type": "view", "path": "firewall/ttl" } } } ================================================ FILE: luci/applications/luci-app-ttl/root/usr/share/rpcd/acl.d/luci-app-ttl.json ================================================ { "luci-app-ttl": { "description": "Grant access to TTL configuration", "read": { "file": { "/etc/init.d/ttl": [ "exec" ], "/etc/init.d/firewall": [ "exec" ] }, "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "changes", "get" ] }, "uci": [ "ttl" ] }, "write": { "cgi-io": [ "exec" ], "ubus": { "file": [ "exec" ], "uci": [ "add", "apply", "confirm", "delete", "order", "rename", "set" ] }, "uci": [ "ttl" ] } } } ================================================ FILE: luci/applications/luci-app-ttl/root/usr/share/ttl.sh ================================================ #!/bin/sh SECTIONS=$(echo $(uci show ttl | awk -F [\]\[\@=] '/=ttl/{print $3}')) get_vars(){ for v in method advanced inet ports ttl iface proxy; do eval $v=$(uci -q get ttl.@ttl[${s}].${v} 2>/dev/nul) done } # check iptables or nft # if [ -x /usr/sbin/iptables -o /usr/sbin/ip6tables -a ! -x /usr/sbin/nft ]; then . /usr/share/ttlipt.sh else . /usr/share/ttlnft.sh fi ================================================ FILE: luci/applications/luci-app-ttl/root/usr/share/ttlipt.sh ================================================ method_ttl(){ ttl=${ttl:=64} #case $(($ttl % 2)) in # 0) TTL_INC=$(($ttl-1)) ;; # *) TTL_INC=$ttl ;; #esac TTL_INC=$(($ttl-1)) for T in $IPT; do case $T in iptables) SUFFIX="TTL --ttl-set" if [ $iface ]; then $T -t mangle -A TTLFIX -i $DEV -m ttl --ttl 1 -j TTL --ttl-inc $TTL_INC else $T -t mangle -A TTLFIX -m ttl --ttl 1 -j TTL --ttl-inc $TTL_INC fi ;; ip6tables) SUFFIX="HL --hl-set" if [ $iface ]; then $T -t mangle -A TTLFIX -i $DEV -m hl --hl 1 -j HL --hl-inc $TTL_INC else $T -t mangle -A TTLFIX -m hl --hl 1 -j HL --hl-inc $TTL_INC fi ;; esac if [ $iface ]; then $T -t mangle -A TTL_OUT -o $DEV -j $SUFFIX $ttl $T -t mangle -A TTL_POST -o $DEV -j $SUFFIX $ttl else $T -t mangle -A TTL_OUT -j $SUFFIX $ttl $T -t mangle -A TTL_POST -j $SUFFIX $ttl fi done } method_proxy(){ for T in $IPT; do [ "$proxy" ] && { IPADDR=${proxy%:*} case $T in iptables) END=${IPADDR}:${proxy#*:} ;; ip6tables) END="[${IPADDR}]:${proxy#*:}" ;; esac } || { case $T in iptables) IPADDR=$(ifstatus $ifn | jsonfilter -e '@["ipv4-address"][*]["address"]') END="${IPADDR}:3128" ;; ip6tables) for a in $(ifstatus $ifn | jsonfilter -e '@["ipv6-prefix-assignment"][*]["local-address"]["address"]'); do IPADDR="$a" done END="[$IPADDR]:3128" ;; esac } $T -t nat -A PROXY -i $DEV -j FIXPROXY case $ports in all) $T -t nat -A FIXPROXY ! -d ${IPADDR} \ ! -s ${IPADDR} -p tcp \ -j DNAT --to-destination $END ;; http) $T -t nat -A FIXPROXY ! -d ${IPADDR} \ ! -s ${IPADDR} -p tcp -m multiport \ --dports 80,443 -j DNAT --to-destination $END ;; *) if [ $ports ]; then $T -t nat -A FIXPROXY ! -d ${IPADDR} \ ! -s ${IPADDR} -p tcp -m multiport \ --dports $ports -j DNAT --to-destination $END else $T -t nat -A FIXPROXY ! -d ${IPADDR} \ ! -s ${IPADDR} -p tcp \ -j DNAT --to-destination $END fi ;; esac done } # check nat66 module if [ -f /lib/modules/$(uname -r)/ip6table_nat.ko ]; then IPT="iptables ip6tables" else IPT="iptables" fi # Create and flush mangle table for T in $IPT; do for t in N F; do for c in TTLFIX TTL_OUT TTL_POST; do $T -t mangle -${t} ${c} done done for a in D I; do $T -t mangle -${a} PREROUTING -j TTLFIX $T -t mangle -${a} OUTPUT -j TTL_OUT $T -t mangle -${a} POSTROUTING -j TTL_POST done done # Create and flush nat table for T in $IPT; do for t in N F; do $T -t nat -${t} PROXY $T -t nat -${t} FIXPROXY done for a in D I; do $T -t nat -${a} PREROUTING -j PROXY done done for s in $SECTIONS; do if [ "$s" ]; then get_vars else exit 0 fi [ -n $iface ] && { ifn=$iface } || { ifn=lan } case $inet in ipv4) IPT="iptables" ;; ipv6) IPT="ip6tables" ;; *) IPT="iptables ip6tables";; esac if ! [ -f /lib/modules/$(uname -r)/ip6table_nat.ko ]; then IPT="iptables" fi DEV=$(ifstatus $iface | jsonfilter -e '@["l3_device"]') case $method in ttl) method_ttl ;; proxy) method_proxy ;; esac done ================================================ FILE: luci/applications/luci-app-ttl/root/usr/share/ttlnft.sh ================================================ method_ttl(){ ttl=${ttl:=64} # create mangle table nft add table ip mangle 2>/dev/null nft add table ip6 mangle 2>/dev/null for fam in $family; do [ "$fam" = "ip6" ] && [ "$inet" = "ipv4" ] && continue [ "$fam" = "ip" ] && [ "$inet" = "ipv6" ] && continue # define nftables chains nft add chain $fam mangle TTLFIX { type filter hook prerouting priority -150 \; } nft add chain $fam mangle TTL_OUT { type route hook output priority -150 \; } nft add chain $fam mangle TTL_POST { type filter hook postrouting priority -150 \; } # TTL/HL change rules case $fam in ip) TTLNAME=ttl ;; ip6) TTLNAME=hoplimit ;; esac if [ $iface ]; then nft add rule $fam mangle TTLFIX iif $DEV $fam $TTLNAME 1 $fam $TTLNAME set $ttl nft add rule $fam mangle TTL_OUT oif $DEV $fam $TTLNAME set $ttl nft add rule $fam mangle TTL_POST oif $DEV $fam $TTLNAME set $ttl else nft add rule $fam mangle TTLFIX $fam $TTLNAME 1 $fam $TTLNAME set $ttl nft add rule $fam mangle TTL_OUT $fam $TTLNAME set $ttl nft add rule $fam mangle TTL_POST $fam $TTLNAME set $ttl fi done } method_proxy(){ for fam in $family; do # create nat table nft add table $fam nat 2>/dev/null [ "$fam" = "ip6" ] && [ "$inet" = "ipv4" ] && continue [ "$fam" = "ip" ] && [ "$inet" = "ipv6" ] && continue [ "$proxy" ] && { IPADDR=${proxy%:*} END=${IPADDR}:${proxy#*:} } || { # get ipaddress from iface if not defined case $fam in ip) IPADDR=$(ifstatus $ifn | jsonfilter -e '@["ipv4-address"][*]["address"]') END="${IPADDR}:3128" ;; ip6) IPADDR=$(ifstatus $ifn | jsonfilter -e '@["ipv6-prefix-assignment"][*]["local-address"]["address"]' | head -n1) END="${IPADDR}:3128" ;; esac } # create NAT chains nft add chain $fam nat PROXY { type nat hook prerouting priority -100 \; } nft add chain $fam nat FIXPROXY # add traffic rule [ $iface ] && { nft add rule $fam nat PROXY iif $DEV jump FIXPROXY } || { nft add rule $fam nat PROXY jump FIXPROXY } case $ports in all) nft add rule $fam nat FIXPROXY $fam daddr != $IPADDR $fam saddr != $IPADDR \ meta l4proto tcp dnat to $END ;; http) nft add rule $fam nat FIXPROXY $fam daddr != $IPADDR $fam saddr != $IPADDR \ meta l4proto tcp tcp dport {80,443} dnat to $END ;; *) if [ $ports ]; then nft add rule $fam nat FIXPROXY $fam daddr != $IPADDR $fam saddr != $IPADDR \ meta l4proto tcp tcp dport {$(echo $ports | tr ',' ',')} dnat to $END else nft add rule $fam nat FIXPROXY $fam daddr != $IPADDR $fam saddr != $IPADDR \ meta l4proto tcp dnat to $END fi ;; esac done } # init tables and chains for fml in ip ip6; do # create table mangle nft delete table $fml mangle 2>/dev/null nft delete table $fml nat 2>/dev/null # define chains nft add table $fml mangle nft add chain $fml mangle TTLFIX { type filter hook prerouting priority -150 \; } nft add chain $fml mangle TTL_OUT { type route hook output priority -150 \; } nft add chain $fml mangle TTL_POST { type filter hook postrouting priority -150 \; } done for s in $SECTIONS; do if [ "$s" ]; then get_vars else exit 0 fi [ $iface ] && { ifn=$iface } || { ifn=lan } case $inet in ipv4) family="ip" ;; ipv6) family="ip6" ;; *) family="ip ip6" ;; esac #if [ ! -f /lib/modules/$(uname -r)/ip6table_nat.ko ]; then # family="ip" #fi DEV=$(ifstatus $iface | jsonfilter -e '@["l3_device"]') case $method in ttl) method_ttl ;; proxy) method_proxy ;; esac done ================================================ FILE: luci/protocols/luci-proto-openvpn/Makefile ================================================ # # Copyright (C) 2026 OpenWRT # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Support for OpenVPN LUCI_DEPENDS:=+openvpn LUCI_PKGARCH:=all PKG_LICENSE:=Apache-2.0 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/protocols/luci-proto-openvpn/htdocs/luci-static/resources/protocol/openvpn.js ================================================ // luci-proto-openvpn: OpenVPN protocol handler for LuCI 'use strict'; 'require form'; 'require fs'; 'require network'; 'require rpc'; 'require uci'; 'require ui'; const callGenKey = rpc.declare({ object: 'luci.openvpn', method: 'generateKey', params: { keytype: 'keytype', ifname: 'ifname', server_key: 'server_key', cl_meta: '' }, }); const callGetSKeys = rpc.declare({ object: 'luci.openvpn', method: 'getSKeys', params: [ 'ifname' ], }); const openvpnOptions = [ /* Basic options (unchanged) */ { tab: 'general', type: form.Value, name: 'port', placeholder: '1194', label: _('TCP/UDP port # for both local and remote') }, { tab: 'general', type: form.Flag, name: 'nobind', label: _('Do not bind to local address and port'), default: 0 }, // --client Options error: specify only one of --tls-server, --tls-client, or --secret // --client also needs DCO(?) { tab: 'general', type: form.Flag, name: 'client', label: _('Configure client mode') + '
' + _('Requires --tls-server, --tls-client, or --secret'), default: 0 }, { tab: 'general', type: form.DynamicList, name: 'remote', datatype: 'or(host,tuple(host,port),tuple(host,port,string))', label: _('Remote host name or IP address'), placeholder: '1.2.3.4' }, { tab: 'general', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'ca', label: _('Certificate authority'), placeholder: '/etc/easy-rsa/keys/ca.crt' }, { tab: 'general', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'cert', label: _('Local certificate'), placeholder: '/etc/easy-rsa/keys/some-client.crt' }, { tab: 'general', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'key', label: _('Local private key'), placeholder: '/etc/easy-rsa/keys/some-client.key' }, { tab: 'general', type: form.Value, name: 'ifconfig', datatype: 'tuple(ipaddr,ipaddr)', placeholder: '10.200.200.3 10.200.200.1', label: _('Set tun/tap adapter parameters') }, { tab: 'general', type: form.Value, name: 'ifconfig_ipv6', datatype: 'or(ip6addr,tuple(ip6addr,ip6addr))', placeholder: 'fd15:53b6:dead::2/64 [fd15:53b6:dead::1]', label: _('Set tun/tap adapter parameters') }, { tab: 'general', type: form.Value, name: 'server', datatype: 'tuple(ipaddr,ipaddr)', placeholder: '10.200.200.0 255.255.255.0', label: _('Configure server mode') }, { tab: 'general', type: form.Value, name: 'server_bridge', datatype: 'tuple(ipaddr,ipaddr,ipaddr,ipaddr)', placeholder: '192.168.1.1 255.255.255.0 192.168.1.128 192.168.1.254', label: _('Configure server bridge') }, /* * { * tab: 'general', * type: form.ListValue, * name: 'comp_lzo', * lvalues: [ * 'yes', * 'no', * 'adaptive' * ], * label: _('Security recommendation: It is recommended to not enable compression and set this parameter to `no`'), * default: 'no' * }, */ { tab: 'general', type: form.Value, name: 'keepalive', placeholder: '10 60', label: _('Helper directive to simplify the expression of --ping and --ping-restart in server mode configurations') }, { tab: 'general', type: form.Flag, name: 'client_to_client', label: _('Allow client-to-client traffic'), default: 0 }, // secret requires --cipher *-CBC { tab: 'basic', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'secret', label: _('Enable Static Key encryption mode (non-TLS)') + '¹' + '
' + _('Used with auth and cipher params'), placeholder: '/etc/openvpn/secret.key' }, { tab: 'basic', type: form.ListValue, name: 'key_direction', values: [ 0, 1 ], label: _('The key direction for \'tls-auth\' and \'secret\' options'), default: 0 }, { tab: 'basic', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'pkcs12', label: _('PKCS#12 file containing keys') + '²', placeholder: '/etc/easy-rsa/keys/some-client.pk12' }, { tab: 'basic', type: form.Value, name: 'peer_fingerprint', label: _('The peer key fingerprint'), placeholder: 'AD:B0:95:D8:09:...' }, /* Devices */ { tab: 'devices', type: form.Value, name: 'dev', placeholder: 'tun0', label: _('tun/tap device') }, // tun == L3 IPv4/6 tap == L2 Ethernet 802.3 { tab: 'devices', type: form.Value, name: 'dev_type', lvalues: [ 'tun', 'tap' ], label: _('Type of used device'), default: 'tun' }, { tab: 'devices', type: form.Value, name: 'dev_node', label: _('Use tun/tap device node'), placeholder: '/dev/net/tun' }, { tab: 'devices', type: form.Flag, name: 'vlan_tagging', label: _('Use VLAN tagging'), default: 0 }, { tab: 'devices', type: form.ListValue, lvalues: [ 'all', 'tagged', 'untagged' ], name: 'vlan_accept', label: _('Accept VLANs') }, { tab: 'devices', type: form.Value, datatype: 'and(uinteger,min(1),max(4094))', name: 'vlan_pvid', label: _('Use these PVIDs') }, /* Service (init/daemon) */ { tab: 'service', type: form.Flag, name: 'mlock', label: _('Disable Paging'), default: 0 }, { tab: 'service', type: form.Flag, name: 'disable_occ', label: _('Disable options consistency check'), default: 0 }, { tab: 'service', type: form.DirectoryPicker, name: 'cd', root_directory: '/etc/openvpn', label: _('Change to directory before initialization'), placeholder: '/etc/openvpn' }, { tab: 'service', type: form.DirectoryPicker, name: 'chroot', root_directory: '/var/run', label: _('Chroot to directory after initialization'), placeholder: '/var/run' }, { tab: 'service', type: form.Flag, name: 'passtos', label: _('TOS passthrough (applies to IPv4 only)'), default: 0 }, { tab: 'service', type: form.Value, name: 'nice', datatype: 'and(integer,min(-20),max(19))', label: _('Change process priority'), placeholder: 0 }, { tab: 'service', type: form.Flag, name: 'fast_io', label: _('Optimize TUN/TAP/UDP writes'), default: 0 }, { tab: 'service', type: form.ListValue, name: 'remap_usr1', lvalues: [ 'SIGHUP', 'SIGTERM' ], label: _('Remap SIGUSR1 signals') }, /* * { * tab: 'service', * type: form.Value, * name: 'status', * placeholder: '/var/run/openvpn.status 5', * label: _('Write status to file every n seconds'), * placeholder: '/var/run/openvpn.status 5' * }, */ { tab: 'service', type: form.ListValue, name: 'status_version', values: [ 1, 2 ], label: _('Status file format version'), default: 2 }, /* * { * tab: 'service', * type: form.ListValue, * name: 'compress', * values: [ * 'frames_only', * 'lzo', * 'lz4', * 'stub-v2' * ], * label: _('Security recommendation: It is recommended to not enable compression and set this parameter to `stub-v2`'), * default: 'stub-v2' * }, */ /* Scripts */ { tab: 'scripts', type: form.ListValue, name: 'script_security', values: [ _('0: Deny'), _('1: OS utils'), _('2: User scripts'), _('3: Allow passwords in env') ], label: _('Policy level over usage of external programs and scripts'), default: 1 }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'up', placeholder: '/usr/bin/ovpn-up', label: _('Shell cmd to execute after tun device open') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'up_delay', placeholder: '5', label: _('Delay tun/tap open and up script execution') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'down', placeholder: '/usr/bin/ovpn-down', label: _('Shell cmd to run after tun device close') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Flag, name: 'down_pre', label: _('Call down cmd/script before TUN/TAP close'), default: 0 }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Flag, name: 'up_restart', label: _('Run up/down scripts for all restarts'), default: 0 }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'route_up', placeholder: '/usr/bin/ovpn-routeup', label: _('Execute shell cmd after routes are added') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'ipchange', placeholder: '/usr/bin/ovpn-ipchange', label: _('Execute shell command on remote IP change') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.DynamicList, name: 'setenv', label: _('Pass environment variables to script'), placeholder: _('name value') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.DynamicList, name: 'setenv_safe', label: _('Pass environment variables to script prepended with OPENVPN_'), placeholder: _('name value') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'tls_verify', placeholder: '/usr/bin/ovpn-tlsverify', label: _('Shell command to verify X509 name') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'client_connect', placeholder: '/usr/bin/ovpn-clientconnect', label: _('Run script cmd on client connection') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'client_crresponse', placeholder: '/usr/bin/ovpn-clientcrresponse', label: _('Run script cmd to validate client certificates') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'client_disconnect', placeholder: '/usr/bin/ovpn-clientdisconnect', label: _('Run script cmd on client disconnection') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'tls_crypt_v2_verify', placeholder: '/usr/bin/ovpn-tlscryptv2verify', label: _('Run script cmd for client TLS verification') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'learn_address', placeholder: '/usr/bin/ovpn-learnaddress', label: _('Executed in server mode whenever an IPv4 address/route or MAC address is added to OpenVPN\'s internal routing table') }, { tab: 'scripts', depends: { script_security: /[1-3]/ }, type: form.Value, name: 'auth_user_pass_verify', placeholder: '/usr/bin/ovpn-userpass via-env', label: _('Executed in server mode on new client connections, when the client is still untrusted') }, /* Logging */ { tab: 'logging', type: form.Value, name: 'echo', label: _('Echo parameters to log'), placeholder: _('some params echoed to log') }, { tab: 'logging', type: form.Value, name: 'log', label: _('Write log to file'), placeholder: '/var/log/openvpn.log' }, { tab: 'logging', type: form.Value, name: 'log_append', label: _('Append log to file'), placeholder: '/var/log/openvpn.log' }, /* * { * tab: 'logging', * type: form.Value, * name: 'syslog', * placeholder: 'openvpn', * label: _('Syslog tag') * }, */ { tab: 'logging', type: form.Value, name: 'mute', label: _('Limit repeated log messages'), placeholder: 5 }, { tab: 'logging', type: form.Flag, name: 'suppress_timestamps', label: _('Don\'t log timestamps'), default: 0 }, { tab: 'logging', type: form.Value, name: 'verb', datatype: 'and(uinteger,min(0),max(11))', label: _('Set output verbosity'), placeholder: '0-11' }, /* Networking (socket, device, routing) */ { tab: 'networking', type: form.ListValue, name: 'mode', lvalues: [ 'p2p', 'server' ], label: _('Major mode') }, { tab: 'networking', type: form.Value, name: 'local', datatype: 'or(hostname,ipaddr)', label: _('Local host name or IP address'), placeholder: '0.0.0.0' }, { tab: 'networking', type: form.Value, name: 'port', datatype: 'port', label: _('TCP/UDP port # for both local and remote'), placeholder: 1194 }, { tab: 'networking', type: form.Value, name: 'lport', datatype: 'port', label: _('TCP/UDP port # for local'), placeholder: 1194 }, { tab: 'networking', type: form.Value, name: 'rport', datatype: 'port', label: _('TCP/UDP port # for remote'), placeholder: 1194 }, // name: 'proto' collides with netifd 'proto' -> map to ovpnproto { tab: 'networking', type: form.Value, name: 'ovpnproto', lvalues: [ 'udp', 'tcp-client', 'tcp-server' ], label: _('Use protocol'), placeholder: 'udp' }, { tab: 'networking', type: form.Flag, name: 'float', label: _('Allow remote to change its IP or port'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'nobind', label: _('Do not bind to local address and port'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'multihome', label: _('When you have more than one IP address (e.g. multiple interfaces, or secondary IP addresses), and do not use --local to force binding to one specific address only'), default: 0 }, /* * { * tab: 'networking', * type: form.Value, * name: 'ifconfig', * label: _('Set tun/tap adapter parameters'), * placeholder: '10.200.200.3 10.200.200.1' * }, */ { tab: 'networking', type: form.Flag, name: 'ifconfig_noexec', label: _('Don\'t actually execute ifconfig'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'ifconfig_nowarn', label: _('Don\'t warn on ifconfig inconsistencies'), default: 0 }, { tab: 'networking', type: form.DynamicList, name: 'route', label: _('Add route after establishing connection'), placeholder: '10.123.0.0 255.255.0.0' }, { tab: 'networking', type: form.Value, name: 'route_gateway', datatype: 'ipaddr', label: _('Specify a placeholder gateway for routes'), placeholder: '10.234.1.1' }, { tab: 'networking', type: form.Value, name: 'route_delay', datatype: 'uinteger', label: _('Delay n seconds after connection'), placeholder: 0 }, { tab: 'networking', type: form.Flag, name: 'route_noexec', label: _('Don\'t add routes automatically'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'route_nopull', label: _('Don\'t pull routes automatically'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'allow_recursive_routing', label: _('Don\'t drop incoming tun packets with same destination as host'), placeholder: 0 }, { tab: 'networking', type: form.ListValue, name: 'mtu_disc', lvalues: [ 'yes', 'maybe', 'no' ], label: _('Enable Path MTU discovery'), default: 'no' }, { tab: 'networking', type: form.Flag, name: 'mtu_test', label: _('Empirically measure MTU'), default: 0 }, /* * { * tab: 'networking', * type: form.ListValue, * name: 'comp_lzo', * lvalues: [ * 'yes', * 'no', * 'adaptive' * ], * label: _('Security recommendation: It is recommended to not enable compression and set this parameter to `no`') + '¹', * default: 'no' * }, */ /* { * tab: 'networking', * type: form.Flag, * name: 'comp_noadapt', * label: _('Don\'t use adaptive lzo compression') + '¹', * placeholder: 0 * }, */ /* { * tab: 'networking', * type: form.Value, * name: 'link_mtu', * datatype: 'uinteger', * label: _('Set TCP/UDP MTU') + '¹', * placeholder: 1500 * }, */ { tab: 'networking', type: form.Value, name: 'tun_mtu', datatype: 'uinteger', label: _('Set tun/tap device MTU'), placeholder: 1500 }, { tab: 'networking', type: form.Value, name: 'tun_mtu_extra', datatype: 'uinteger', label: _('Set tun/tap device overhead'), placeholder: 1500 }, { tab: 'networking', type: form.Value, name: 'fragment', datatype: 'uinteger', label: _('Enable internal datagram fragmentation'), placeholder: 1500 }, { tab: 'networking', type: form.Value, name: 'mssfix', datatype: 'uinteger', label: _('Set upper bound on TCP MSS'), placeholder: 1450 }, { tab: 'networking', type: form.Value, name: 'sndbuf', datatype: 'uinteger', label: _('Set the TCP/UDP send buffer size'), placeholder: 65536 }, { tab: 'networking', type: form.Value, name: 'rcvbuf', datatype: 'uinteger', label: _('Set the TCP/UDP receive buffer size'), placeholder: 65536 }, { tab: 'networking', type: form.Value, name: 'txqueuelen', datatype: 'uinteger', label: _('Set tun/tap TX queue length'), placeholder: 100 }, { tab: 'networking', type: form.Value, name: 'shaper', datatype: 'uinteger', label: _('Shaping for peer bandwidth'), placeholder: 10240 }, { tab: 'networking', type: form.Value, name: 'inactive', datatype: 'uinteger', label: _('tun/tap inactivity timeout'), placeholder: 240 }, { tab: 'networking', type: form.Value, name: 'keepalive', label: _('Helper directive to simplify the expression of --ping and --ping-restart in server mode configurations'), placeholder: '10 60' }, { tab: 'networking', type: form.Value, name: 'ping', datatype: 'uinteger', label: _('Ping remote every n seconds over TCP/UDP port'), placeholder: 30 }, { tab: 'networking', type: form.Value, name: 'ping_exit', datatype: 'uinteger', label: _('Remote ping timeout'), placeholder: 120 }, { tab: 'networking', type: form.Value, name: 'ping_restart', datatype: 'uinteger', label: _('Restart after remote ping timeout'), placeholder: 60 }, { tab: 'networking', type: form.Flag, name: 'ping_timer_rem', label: _('Only process ping timeouts if routes exist'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'persist_tun', label: _('Keep tun/tap device open on restart'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'persist_key', label: _('Don\'t re-read key on restart'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'persist_local_ip', label: _('Keep local IP address on restart'), default: 0 }, { tab: 'networking', type: form.Flag, name: 'persist_remote_ip', label: _('Keep remote IP address on restart'), default: 0 }, /* Management */ { tab: 'management', type: form.Value, name: 'management', label: _('Enable management interface on IP port'), placeholder: '127.0.0.1 31194 /etc/openvpn/mngmt-pwds' }, { tab: 'management', type: form.Flag, name: 'management_query_passwords', label: _('Query management channel for private key'), default: 0 }, { tab: 'management', type: form.Flag, name: 'management_hold', label: _('Start OpenVPN in a hibernating state'), default: 0 }, { tab: 'management', type: form.Value, name: 'management_log_cache', datatype: 'uinteger', label: _('Number of lines for log file history'), placeholder: 100 }, { tab: 'management', type: form.Value, name: 'management_external_cert', datatype: 'file', label: _('Management cert'), placeholder: 'certificate-hint' }, { tab: 'management', type: form.Value, name: 'management_external_key', datatype: 'file', label: _('Management key'), placeholder: 'nopadding pkcs1' }, /* Topology */ { tab: 'topology', type: form.ListValue, name: 'topology', lvalues: [ 'net30', 'p2p', 'subnet' ], label: _('\'net30\', \'p2p\', or \'subnet\''), default: '' }, { tab: 'topology', type: form.Flag, name: 'disable_dco', label: _('Disable Data Channel Offloading (DCO) support'), default: 0 }, /* Crypto */ { tab: 'cryptography', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'dh', label: _('Diffie-Hellman parameters'), placeholder: '/etc/easy-rsa/keys/dh1024.pem' }, { tab: 'cryptography', type: form.DirectoryPicker, root_directory: '/etc', name: 'capath', label: _('CA path') + '²' }, { tab: 'cryptography', type: form.FileUpload, root_directory: '/etc', name: 'askpass_file', label: _('Get certificate password from file before we daemonize') }, { tab: 'cryptography', type: form.Value, name: 'auth', label: _('HMAC authentication for packets'), lvalues: [ 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512', 'SHA3-224', 'SHA3-256', 'SHA3-384', 'SHA3-512', 'none' ] }, // --data-ciphers-fallback with cipher 'AES-256-CBC' disables data channel offload. // for --secret pre-shared-key mode. Ignored in >= 2.6 (TLS mode). { tab: 'cryptography', type: form.Value, name: 'cipher', label: _('Encryption cipher for packets') + '¹', lvalues: [ 'AES-128-CBC', 'AES-128-GCM', 'AES-192-CBC', 'AES-192-GCM', 'AES-256-CBC', 'AES-256-GCM', 'CHACHA20-POLY1305' ] }, /* * { * tab: 'cryptography', * type: form.Value, * name: 'keysize', * label: _('Size of cipher key'), * placeholder: 1024 * }, */ { tab: 'cryptography', type: form.Value, name: 'engine', label: _('Enable OpenSSL hardware crypto engines') + '²', lvalues: [ 'dynamic' ] }, { tab: 'cryptography', type: form.Value, name: 'ecdh_curve', label: _('Specify the curve to use for ECDH') + '²', lvalues: [ 'x25519', 'secp521r1', 'secp384r1', 'secp256r1', 'secp256k1' ] }, { tab: 'cryptography', type: form.Value, name: 'replay_window', label: _('Replay protection sliding window size'), placeholder: '64 15' }, { tab: 'cryptography', type: form.Value, name: 'replay_window', label: _('Replay protection sliding window size'), placeholder: '64 15' }, { tab: 'cryptography', type: form.Flag, name: 'mute_replay_warnings', label: _('Silence the output of replay warnings'), default: 0 }, { tab: 'cryptography', type: form.FileUpload, root_directory: '/var/run/', name: 'replay_persist', label: _('Persist replay-protection state'), placeholder: '/var/run/openvpn-replay-state' }, { tab: 'cryptography', type: form.Flag, name: 'tls_server', label: _('Enable TLS and assume server role'), default: 0 }, // depends: { tls_client: 0} { tab: 'cryptography', type: form.Flag, name: 'tls_client', label: _('Enable TLS and assume client role'), default: 0 }, // depends: { tls_server: 0} /* * { * tab: 'cryptography', * type: form.Value, * name: 'key_method', * label: _('Enable TLS and assume client role') * }, */ { tab: 'cryptography', type: form.DynamicList, name: 'tls_cipher', label: _('TLS cipher'), lvalues: [ 'TLS1-3-CHACHA20-POLY1305-SHA256', 'TLS1-3-AES-256-GCM-SHA384', 'TLS1-3-AES-128-GCM-SHA256', 'TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384', 'TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384', 'TLS-DHE-RSA-WITH-AES-256-GCM-SHA384', 'TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256', 'TLS-ECDHE-RSA-WITH-CHACHA20-POLY1305-SHA256', 'TLS-DHE-RSA-WITH-CHACHA20-POLY1305-SHA256', 'TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256', 'TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256', 'TLS-DHE-RSA-WITH-AES-128-GCM-SHA256', 'TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA384', 'TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA384', 'TLS-DHE-RSA-WITH-AES-256-CBC-SHA256', 'TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256', 'TLS-ECDHE-RSA-WITH-AES-128-CBC-SHA256', 'TLS-DHE-RSA-WITH-AES-128-CBC-SHA256', 'TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA', 'TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA', 'TLS-DHE-RSA-WITH-AES-256-CBC-SHA', 'TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA', 'TLS-ECDHE-RSA-WITH-AES-128-CBC-SHA', 'TLS-DHE-RSA-WITH-AES-128-CBC-SHA', 'TLS-ECDHE-PSK-WITH-CHACHA20-POLY1305-SHA256', 'TLS-ECDHE-PSK-WITH-AES-256-CBC-SHA384', 'TLS-ECDHE-PSK-WITH-AES-256-CBC-SHA', 'TLS-ECDHE-PSK-WITH-AES-128-CBC-SHA256', 'TLS-ECDHE-PSK-WITH-AES-128-CBC-SHA', 'TLS-PSK-WITH-CHACHA20-POLY1305-SHA256', 'TLS-PSK-WITH-AES-256-GCM-SHA384', 'TLS-PSK-WITH-AES-256-CBC-SHA384', 'TLS-PSK-WITH-AES-256-CBC-SHA', 'TLS-PSK-WITH-AES-128-GCM-SHA256', 'TLS-PSK-WITH-AES-128-CBC-SHA256', 'TLS-PSK-WITH-AES-128-CBC-SHA' ] }, { tab: 'cryptography', type: form.DynamicList, name: 'tls_ciphersuites', label: _('TLS 1.3 or newer cipher'), lvalues: [ 'TLS_AES_256_GCM_SHA384', 'TLS_AES_128_GCM_SHA256', 'TLS_CHACHA20_POLY1305_SHA256' ] }, { tab: 'cryptography', type: form.Value, name: 'tls_timeout', datatype: 'uinteger', label: _('Retransmit timeout on TLS control channel'), placeholder: 2 }, { tab: 'cryptography', type: form.Value, name: 'reneg_bytes', datatype: 'uinteger', label: _('Renegotiate data chan. key after bytes'), placeholder: 1024 }, { tab: 'cryptography', type: form.Value, name: 'reneg_pkts', datatype: 'uinteger', label: _('Renegotiate data chan. key after packets'), placeholder: 100 }, { tab: 'cryptography', type: form.Value, name: 'reneg_sec', datatype: 'uinteger', label: _('Renegotiate data chan. key after seconds'), placeholder: 3600 }, { tab: 'cryptography', type: form.Value, name: 'hand_window', datatype: 'uinteger', label: _('Timeframe for key exchange'), placeholder: 60 }, { tab: 'cryptography', type: form.Value, name: 'tran_window', datatype: 'uinteger', label: _('Key transition window'), placeholder: 3600 }, { tab: 'cryptography', type: form.Flag, name: 'single_session', datatype: 'uinteger', label: _('Allow only one session'), default: 0 }, { tab: 'cryptography', type: form.Flag, name: 'tls_exit', datatype: 'uinteger', label: _('Exit on TLS negotiation failure'), default: 0 }, { tab: 'cryptography', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'tls_auth', label: _('Additional authentication over TLS'), placeholder: '/etc/openvpn/tlsauth.key' }, { tab: 'cryptography', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'tls_crypt', label: _('Encrypt and authenticate all control channel packets with the key'), placeholder: '/etc/openvpn/tlscrypt.key' }, { tab: 'cryptography', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'tls_crypt_v2', label: _('Encrypt and authenticate all control channel packets with the key, version 2.'), placeholder: '/etc/openvpn/servertlscryptv2.key' }, { tab: 'cryptography', type: form.Flag, name: 'auth_nocache', label: _('Don\'t cache --askpass or --auth-user-pass passwords'), default: 0 }, /* * { * tab: 'cryptography', * type: form.Value, * name: 'tls_remote', * label: _('Only accept connections from given X509 name'), * placeholder: 'remote_x509_name' * }, */ { tab: 'cryptography', type: form.ListValue, name: 'ns_cert_type', lvalues: [ 'client', 'server' ], label: _('Require explicit designation on certificate') }, { tab: 'cryptography', type: form.Value, name: 'remote_cert_eku', label: _('Require remote cert extended key usage on certificate'), placeholder: 'oid' }, { tab: 'cryptography', type: form.Value, name: 'remote_cert_ku', label: _('Require explicit key usage on certificate'), placeholder: 'a0' }, { tab: 'cryptography', type: form.ListValue, name: 'remote_cert_tls', lvalues: [ 'client', 'server' ], label: _('Require explicit key usage on certificate') }, { tab: 'cryptography', type: form.FileUpload, root_directory: '/etc/openvpn', name: 'crl_verify', label: _('Check peer certificate against a CRL'), placeholder: '/etc/easy-rsa/keys/crl.pem' }, { tab: 'cryptography', type: form.Value, name: 'tls_version_min', label: _('The lowest supported TLS version'), lvalues: [ '1.2', '1.3' ] }, { tab: 'cryptography', type: form.Value, name: 'tls_version_max', label: _('The highest supported TLS version'), lvalues: [ '1.2', '1.3' ] }, { tab: 'cryptography', type: form.Value, name: 'tls_cert_profile', label: _('TLS cet profile'), lvalues: [ 'insecure', 'legacy', 'preferred', 'suiteb' ] }, /* * { * tab: 'cryptography', * type: form.Flag, * name: 'ncp_disable', * label: _('This completely disables cipher negotiation'), * default: 0 * }, */ /* * { * tab: 'cryptography', * type: form.DynamicList, * name: 'ncp_ciphers', * label: _('Restrict the allowed ciphers to be negotiated'), * lvalues: [ * 'AES-256-GCM', * 'AES-128-GCM' * ] * }, */ { tab: 'cryptography', type: form.DynamicList, name: 'data_ciphers', label: _('Restrict the allowed ciphers to be negotiated'), lvalues: [ 'CHACHA20-POLY1305', 'AES-256-GCM', 'AES-128-GCM', 'AES-256-CBC' ] }, /* Push/Client */ { tab: 'push_opt', type: form.DynamicList, name: 'push', label: _('Push options to peer'), lvalues: ['redirect-gateway'] }, { tab: 'push_opt', type: form.DynamicList, name: 'push_remove', label: _('Remove Push options'), }, { tab: 'push_opt', type: form.Flag, name: 'push_reset', label: _('Don\'t inherit global push options'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'disable', label: _('Client is disabled'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'ifconfig_pool', label: _('Set aside a pool of subnets') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'ifconfig_pool_persist', label: _('Persist/unpersist ifconfig-pool') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'ifconfig_push', label: _('Push an ifconfig option to remote') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'iroute', label: _('Route subnet to client') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'iroute_ipv6', label: _('Route v6 subnet to client') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'duplicate_cn', label: _('Allow multiple clients with same certificate'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.DirectoryPicker, root_directory: '/etc/openvpn', directory_create: true, name: 'client_config_dir', label: _('Directory for custom client config files') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'ccd_exclusive', label: _('Refuse connection if no custom client config'), default: 0 }, /* * { * tab: 'push_opt', * depends: { server: "", "!reverse": true }, * type: form.DirectoryPicker, * root_directory: '/var/run', * directory_create: true, * name: 'tmp_dir', * label: _('Temporary directory for client-connect return file') * }, */ { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'hash_size', label: _('Set size of real and virtual address hash tables') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'bcast_buffers', label: _('Number of allocated broadcast buffers') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'tcp_queue_limit', label: _('Maximum number of queued TCP output packets') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'max_clients', label: _('Allowed maximum of connected clients') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'max_routes_per_client', label: _('Allowed maximum of internal') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'connect_freq', label: _('Allowed maximum of new connections') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'username_as_common_name', label: _('Use username as common name'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'pull', label: _('Accept options pushed from server'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'auth_user_pass', label: _('Authenticate using username/password') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'auth_retry', label: _('Handling of authentication failures') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'explicit_exit_notify', label: _('Send notification to peer on disconnect') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'remote_random', label: _('Randomly choose remote server'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'connect_retry', label: _('Connection retry interval') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'http_proxy', label: _('Connect to remote host through an HTTP proxy') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Flag, name: 'http_proxy_retry', label: _('Retry indefinitely on HTTP proxy errors'), default: 0 }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'http_proxy_timeout', label: _('Proxy timeout in seconds') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.DynamicList, name: 'http_proxy_option', label: _('Set extended HTTP proxy options') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'socks_proxy', label: _('Connect through Socks5 proxy') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'socks_proxy_retry', label: _('Retry indefinitely on Socks proxy errors') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'resolv_retry', label: _('If hostname resolve fails, retry') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'redirect_gateway', label: _('Automatically redirect default route') }, { tab: 'push_opt', depends: { server: "", "!reverse": true }, type: form.Value, name: 'verify_client_cert', label: _('Specify whether the client is required to supply a valid certificate') }, ]; // Tabs for UI var tabs = [ { id: 'basic', label: _('Basic Settings') }, { id: 'file', label: _('Config File') }, { id: 'general' }, { id: 'cryptography', label: _('Cryptography') }, { id: 'devices', label: _('Devices') }, { id: 'expert', label: _('Expert Settings') }, { id: 'keygen', label: _('Keygen') }, { id: 'logging', label: _('Logging') }, { id: 'management', label: _('Management') }, { id: 'networking', label: _('Networking') }, { id: 'push_opt', label: _('Push') }, { id: 'scripts', label: _('Scripting') }, { id: 'service', label: _('Service') }, { id: 'topology', label: _('Topology') }, ]; function renderOpenVPNOptions(s, tabId) { openvpnOptions.filter(function(opt) { return opt.tab === tabId; }).forEach(function(opt) { let o = s.taboption(tabId, opt.type, opt.name, opt.name, opt.label); if (opt.values) { // index based values (o.value(i, string)) opt.values.forEach((v, i) => o.value(i, v)); } else if (opt.lvalues) { // literal values: the text string is the value opt.lvalues.forEach((v) => o.value(v)); } // Copy any extra properties to the widget Object.keys(opt).forEach(function(key) { if (!['tab', 'type', 'name', 'label', 'values', 'lvalues'].includes(key)) { if (key === 'depends') o.depends(opt[key]) else o[key] = opt[key]; } }); o.optional = true; }); } network.registerPatternVirtual(/^openvpn-.+$/); return network.registerProtocol('openvpn', { getI18n: function() { return _('OpenVPN'); }, getIfname: function() { return this._ubus('l3_device') || this.sid; }, getPackageName: function() { return 'openvpn-openssl'; }, isFloating: function() { return true; }, isVirtual: function() { return true; }, getDevices: function() { return null; }, containsDevice: function(ifname) { return (network.getIfnameOf(ifname) == this.getIfname()); }, renderFormOptions: function(s) { let o, kt, sk, krp, krc, clm, ovconf; // Add main tabs tabs.forEach(function(tab) { if (!s.tabs[tab.id]) s.tab(tab.id, tab.label); }); o = s.taboption('general', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = _('Almost nothing here prevents you from selecting invalid configuration options which prevent openvpn from starting. Read the manual.') + '
' + _('Options marked with ¹ are deprecated and will be removed.') /* + '
' + _('Options marked with * are server only.') */ + '
' + _('Options marked with ² are OpenSSL only.'); // Render options for each tab tabs.forEach(function(tab) { renderOpenVPNOptions(s, tab.id); }); kt = s.taboption('keygen', form.ListValue, '_keytype', _('Type')); kt.default = 'secret'; kt.value('secret'); kt.value('tls-crypt'); kt.value('tls-auth'); kt.value('auth-token'); kt.value('tls-crypt-v2-server'); kt.value('tls-crypt-v2-client'); kt.write = function(section_id, value) {}; sk = s.taboption('keygen', form.Value, '_skey', _('Server key')); callGetSKeys(s.map.section).then(keys => { if (keys.skeys) { for (let key of keys.skeys) sk.value(key); } }); clm = s.taboption('keygen', form.TextValue, '_cmeta', _('Client metadata'), _('Freeform metadata to embed into the client key')); clm.depends('_keytype', 'tls-crypt-v2-client'); clm.default = ''; clm.placeholder = '{"cn":"alice","exp":1735689600}'; clm.monospace = true; clm.rows = 10; clm.wrap = 90; o = s.taboption('keygen', form.Button, '_keygen', _('Generate')); o.onclick = L.bind(function(ev, sid) { const ktype = kt.formvalue(sid); const svk = sk.formvalue(sid); const clmeta = clm.formvalue(sid); callGenKey({ ifname: sid, keytype: ktype, server_key: svk ?? '', cl_meta: btoa(clmeta) }).then(result => { const path_output = krp.getUIElement(sid); const cont_output = krc.getUIElement(sid); path_output.setValue(result.path); cont_output.setValue(result.content); if (ktype === 'tls-crypt-v2-server') { const skv = sk.getUIElement(sid); skv.setValue(result.filename); } }); }, this); krp = s.taboption('keygen', form.TextValue, '_pathresult', _('Path')); krp.default = ''; krp.monospace = true; krp.readonly = true; krp.readonly = true; krp.rows = 2; krp.wrap = 90; krp.write = function(sid, value) { return; }; krc = s.taboption('keygen', form.TextValue, '_contentresult'); krc.default = ''; krc.monospace = true; krc.readonly = true; krc.rows = 10; krc.wrap = 90; krc.write = function(sid, value) { return; }; o = s.taboption('file', form.Button, '_clear', _('Clear')); o.onclick = L.bind(function(ev, sid) { const config_name = `/etc/openvpn/${sid}/${sid}_config.cfg`; let v = ovconf.getUIElement(sid); // remove file, clear field, mark it changed and save fs.remove(config_name).catch(() => {}).then(() => { try { v.setValue(''); if (v && v.node) v.node.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} uci.unset(this.config, sid, 'config'); if (s && s.map) s.map.save(null, true); }); }, this); ovconf = s.taboption('file', form.TextValue, 'ovpn_config', _('Raw OVPN config')); ovconf.rows = 20; ovconf.optional = true; ovconf.placeholder = _('Drag and drop an ovpn config file here'); // Attach drag-and-drop handler for file import function attachDragDrop(sid) { try { // If the map root is not yet available, retry a few times with back-off ovconf._attach_attempts = ovconf._attach_attempts || {}; ovconf._attach_attempts[sid] = (ovconf._attach_attempts[sid] || 0) + 1; if (!s || !s.map || !s.map.root) { if (ovconf._attach_attempts[sid] < 10) { setTimeout(() => attachDragDrop(sid), 1000); } return; } const w = ovconf.getUIElement(sid); if (!w || !w.node || w._ovpn_drop_attached) { if (ovconf._attach_attempts[sid] < 10) setTimeout(() => attachDragDrop(sid), 1000); return; } const ta = w.node.firstElementChild; if (!ta) { return; } ta.addEventListener('dragover', ev => { ev.preventDefault(); ev.dataTransfer.dropEffect = 'copy'; ta.classList.add('cbi-dragover'); }); ta.addEventListener('dragleave', ev => { ev.preventDefault(); ta.classList.remove('cbi-dragover'); }); ta.addEventListener('drop', ev => { ev.preventDefault(); ev.stopPropagation(); ta.classList.remove('cbi-dragover'); const files = ev.dataTransfer && (ev.dataTransfer.files || ev.dataTransfer.items); let file = files && files[0]; if (files && files[0] && files[0].kind === 'file' && files[0].getAsFile) file = files[0].getAsFile(); if (!file) { // fallback: try plain text data const text = ev.dataTransfer && (ev.dataTransfer.getData && (ev.dataTransfer.getData('text') || ev.dataTransfer.getData('text/plain'))); if (text) { w.setValue(text); w.node.dispatchEvent(new Event('change', { bubbles: true })); } return; } const reader = new FileReader(); reader.onload = function() { const content = reader.result; w.setValue(content); w.node.dispatchEvent(new Event('change', { bubbles: true })); }; reader.onerror = function(err) { console.error('ovpn file read error', err); }; reader.readAsText(file); }); w._ovpn_drop_attached = true; } catch (e) { } } ovconf.cfgvalue = function(sid) { attachDragDrop(sid); const config_name = `/etc/openvpn/${sid}/${sid}_config.cfg`; return fs.read(config_name).then(readresult => { attachDragDrop(sid); if (readresult == null || readresult === '') { uci.unset(this.config, sid, 'config'); return ''; } uci.set(this.config, sid, 'config', config_name); return readresult; }).catch(() => { attachDragDrop(sid); uci.unset(this.config, sid, 'config'); return ''; }); }; ovconf.write = function(sid, value) { const config_name = `/etc/openvpn/${sid}/${sid}_config.cfg`; return fs.exec('/bin/mkdir', ['-p', `/etc/openvpn/${sid}/`]).then(() => { return fs.write(config_name, value); }).then(() => { try { uci.set(this.config, sid, 'config', config_name); } catch (err) {} }); }; ovconf.rmempty = true; }, deleteConfiguration: function() { uci.sections('network', 'openvpn_%s'.format(this.sid), function(s) { uci.remove('network', s['.name']); }); } }); ================================================ FILE: luci/protocols/luci-proto-openvpn/root/usr/share/rpcd/acl.d/luci-proto-openvpn.json ================================================ { "luci-proto-openvpn": { "description": "Grant access to LuCI openvpn procedures", "read": { "file": { "/bin/mkdir -p /etc/openvpn/*": [ "exec" ], "/etc/openvpn/*": [ "read" ] }, "ubus": { "luci.openvpn": [ "*" ] }, "uci": [ "network" ] }, "write": { "file": { "/etc/openvpn/*": [ "write", "remove" ] }, "ubus": { "luci.openvpn": [ "*" ] }, "uci": [ "network" ] } } } ================================================ FILE: luci/protocols/luci-proto-openvpn/root/usr/share/rpcd/ucode/luci.openvpn.uc ================================================ #!/usr/bin/env ucode 'use strict'; import { stdin, access, chmod, dirname, basename, open, popen, glob, lsdir, readfile, readlink, error, mkdir } from 'fs'; import { cursor } from 'uci'; import { connect } from 'ubus'; const openvpn_dir = '/etc/openvpn'; function shellquote(s) { return `'${replace(s, "'", "'\\''")}'`; } function command(cmd) { return trim(popen(cmd)?.read?.('all')); } function makedirs(path) { const parts = split(path, '/'); const grow = []; for(let i = 0; i < length(parts); i++) { push(grow, parts[i]); mkdir(join('/', grow)); } return join('/', grow); } function keyDir(ifname, kt) { return `${openvpn_dir}/${ifname}/${kt}`; } const methods = { generateKey: { args: { keytype: 'keytype', ifname: 'ifname', server_key: 'server_key', cl_meta: 'c_metadata' }, call: function(req) { const kt = req.args?.keytype; const ifname = req.args?.ifname || 'unnamed'; const ts = time(); if (!kt) return { error: 'missing keytype' }; let dir; let outfile = `${ifname}_${kt}_${ts}.key`; let mkpath = makedirs(`${keyDir(ifname, kt)}`); let path = `${mkpath}/${outfile}`; // find openvpn binary const openvpn = trim(popen('command -v openvpn 2>/dev/null')?.read?.('all')); if (!length(openvpn)) return { error: 'openvpn binary not found' }; let cmd; if (kt == 'tls-crypt-v2-client') { // client generation needs server key let server_key = req.args?.server_key; if (!server_key) { // try to pick latest server key for same interface const serverDir = `${keyDir(ifname, 'tls-crypt-v2-server')}`; const list = lsdir(serverDir); if (length(list) > 0) server_key = serverDir + '/' + list[-1]; } else { server_key = `${keyDir(ifname, 'tls-crypt-v2-server')}/${req.args?.server_key}`; } if (!server_key) return { error: 'missing server_key for tls-crypt-v2-client' }; // denote which server key this client key is derived from in the name path = `${mkpath}/${ifname}_${kt}_${ts}-${req.args?.server_key}`; cmd = `${openvpn} --tls-crypt-v2 ${shellquote(server_key)} --genkey tls-crypt-v2-client ${shellquote(path)} ${req.args?.cl_meta} 2>/dev/null`; } else { // basic genkey cmd = `${openvpn} --genkey ${kt} ${shellquote(path)} 2>/dev/null`; } const out = popen(cmd)?.read?.('all') || ''; // ensure permissions chmod(path, 0o600); let content = ''; try { content = readfile(path); } catch (e) { /* ignore */ } if (!length(content)) { return { error: 'failed to generate key', cmd_output: out }; } return { path, filename: outfile, content }; } }, getSKeys: { args: { ifname: 'ifname' }, call: function(req) { const serverDir = `${keyDir(req.args?.ifname, 'tls-crypt-v2-server')}`; const list = lsdir(serverDir); return { skeys: list, path: serverDir }; } } }; return { 'luci.openvpn': methods }; ================================================ FILE: luci/protocols/luci-proto-tun2socks/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Support for tun2socks LUCI_DEPENDS:=+tun2socks include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/protocols/luci-proto-tun2socks/htdocs/luci-static/resources/protocol/t2s.js ================================================ 'use strict'; 'require rpc'; 'require form'; 'require network'; 'require validation'; network.registerPatternVirtual(/^t2s-.+$/); return network.registerProtocol('t2s', { getI18n: function() { return _('tun2socks'); }, getIfname: function() { return this._ubus('l3_device') || 't2s-%s'.format(this.sid); }, getOpkgPackage: function() { return 'tun2socks'; }, isFloating: function() { return true; }, isVirtual: function() { return true; }, getDevices: function() { return null; }, containsDevice: function(ifname) { return (network.getIfnameOf(ifname) == this.getIfname()); }, renderFormOptions: function(s) { var dev = this.getL3Device() || this.getDevice(), o; o = s.taboption('general', form.ListValue, 'proxy', _('Proxy Type')); o.value('http', 'HTTP'); o.value('socks4', 'SOCKS4'); o.value('socks5', 'SOCKS5'); o.value('ss', 'Shadowsocks'); o.value('relay', _('Relay')); o.value('direct', _('Direct')); o.value('reject', _('Reject')); o.default = 'socks5'; o.rmempty = true; // TODO o = s.taboption('general', form.Flag, 'socket', _('Use Socket'), _('SOCKS5 only!
Use Unix Domain Socket instead address')); o.rmempty = true; o = s.taboption('general', form.Value, 'host', _('Proxy Address'), _('IP-address or FQDN hostname proxy
Format: host:port')); o.datatype = 'or(hostport,ipaddrport)'; o.depends({'socket': '0', 'proxy': /http|socks4|socks5|relay|ss/ }); o.rmempty = true; o = s.taboption('general', form.Value, 'sockpath', _('Unix Socket'), _('Path to Unix Socket
Format: /path/to/unix.socket')); o.depends({'socket': '1' , 'proxy': 'socks5' }); o = s.taboption('general', form.Flag, 'advanced', _('Autentification'), _('Authentification and encryption.')); o.depends({'proxy': /socks4|socks5|relay|ss/ }); o.rmempty = true; o = s.taboption('general', form.Value, 'username', _('Proxy USER')); o.depends({'advanced': '1', 'proxy': /socks|relay/}); o = s.taboption('general', form.ListValue, 'encrypt', _('Encryption')); o.value('none','none'); o.value('table','table'); o.value('rc4','rc4'); o.value('rc4-md5','rc4-md5'); o.value('aes-128-cfb','aes-128-cfb'); o.value('aes-192-cfb','aes-192-cfb'); o.value('aes-256-cfb','aes-256-cfb'); o.value('aes-128-ctr','aes-128-ctr'); o.value('aes-192-ctr','aes-192-ctr'); o.value('aes-256-ctr','aes-256-ctr'); o.value('aes-128-gcm','aes-128-gcm'); o.value('aes-192-gcm','aes-192-gcm'); o.value('aes-256-gcm','aes-256-gcm'); o.value('camellia-128-cfb','camellia-128-cfb'); o.value('camellia-192-cfb','camellia-192-cfb'); o.value('camellia-256-cfb','camellia-256-cfb'); o.value('bf-cfb','bf-cfb'); o.value('salsa20','salsa20'); o.value('chacha20','chacha20'); o.value('chacha20-ietf','chacha20-ietf'); o.value('chacha20-ietf-poly1305','chacha20-ietf-poly1305'); o.value('xchacha20-ietf-poly1305','xchacha20-ietf-poly1305'); o.depends({advanced: '1', proxy: 'ss'}); o = s.taboption('general', form.Value, 'password', _('Proxy Password')); o.password = true; o.depends({'advanced': '1', 'proxy': /socks5|relay|ss/ }); o = s.taboption('general',form.Flag, 'base64enc', _('Encrypt base64')); o.depends({'advanced': '1', 'proxy': 'ss' }); o.rmempty = true; o = s.taboption('general', form.Flag, 'ip_manual', _('Assigh Address Automatically')); o.enabled = ''; o.disabled = '1'; o.default = ''; o = s.taboption('general', form.Value, 'network', _('Select class net by assign Address'), _('By default is 10.0.0.0/8')); o.value('10.0.0.0/8', '10.0.0.0/8'); o.value('172.16.0.0/12', '172.16.0.0/12'); o.value('192.168.0.0/16', '192.168.0.0/16'); o.default = '10.0.0.0/8'; o.depends({'ip_manual': ''}); o = s.taboption('general', form.Value, 'ipaddr', _('IPv4 Address')); o.datatype = 'ip4addr("nomask")'; o.depends({'ip_manual': '1', 'proxy': /http|socks4|socks5|relay|ss/ }); o.rmempty = true; o = s.taboption('general', form.Value, 'netmask', _('IPv4 Netmask')); o.value('255.255.255.0', '255.255.255.0'); o.value('255.255.0.0', '255.255.0.0'); o.depends({'ip_manual': '1', 'proxy': /http|socks4|socks5|relay|ss/ }); o.rmempty = true; o = s.taboption('general', form.Value, 'gateway', _('IPv4 Gateway')); o.datatype = 'ip4addr("nomask")'; o.depends({'ip_manual': '1', 'proxy': /http|socks4|socks5|relay|ss/ }); o.rmempty = true; o = s.taboption('advanced', form.Value, 'mtu', _('Set MTU'), _('Set device maximum transmission unit')); o.placeholder = dev ? (dev.getMTU() || '1500') : '1500'; o.datatype = 'max(9200)'; o = s.taboption('advanced', form.ListValue, 'loglevel', _('Logging level')); o.value('debug', _('Debug')); o.value('info', _('Info')); o.value('warning', _('Warning')); o.value('error', _('Error')); o.value('silent', _('Silent')); o.default = 'error'; o = s.taboption('advanced', form.Value, 'opts', _('Advaced options'), _('Command line arguments to tun2socks app')); o.rmempty = true; o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured')); o.default = o.enabled; o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric')); o.placeholder = '0'; o.datatype = 'uinteger'; o.depends('defaultroute', '1'); o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored')); o.default = o.enabled; } }); ================================================ FILE: luci/protocols/luci-proto-xmm/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Support for Intel XMM LUCI_DEPENDS:=+xmm-modem include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/protocols/luci-proto-xmm/README.md ================================================ # luci-proto-xmm The OpenWrt Luci protocol handler to configure Intel cellular XMM modems.
Screenshots ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/protocols/luci-proto-xmm/screenshots/main.png) ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/protocols/luci-proto-xmm/screenshots/interfaces.png) ![](https://raw.githubusercontent.com/koshev-msk/modemfeed/master/luci/protocols/luci-proto-xmm/screenshots/setup.png)
================================================ FILE: luci/protocols/luci-proto-xmm/htdocs/luci-static/resources/protocol/xmm.js ================================================ 'use strict'; 'require rpc'; 'require form'; 'require network'; var callFileList = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ], expect: { entries: [] }, filter: function(list, params) { var rv = []; for (var i = 0; i < list.length; i++) if (list[i].name.match(/^ttyACM/) || list[i].name.match(/^ttyUSB/)) rv.push(params.path + list[i].name); return rv.sort(); } }); network.registerPatternVirtual(/^xmm-.+$/); network.registerErrorCode('NO_DEVICE_SUPPORT', _('Unsupported modem')); network.registerErrorCode('NO_PORT_FOUND', _('No control device specified')); network.registerErrorCode('NO_PORT_ANSWER', _('Failed to get modem information')); network.registerErrorCode('NO_DEVICE_FOUND', _('Failed to initialize modem')); network.registerErrorCode('NO_IFACE', _('The interface could not be found')); network.registerErrorCode('NO_SIM_CARD', _('SIM-card not insert!')); network.registerErrorCode('CONFIGURE_FAILED', _('Failed to configure modem')); return network.registerProtocol('xmm', { getI18n: function() { return _('Intel XMM Cellular'); }, getIfname: function() { return this._ubus('l3_device') || 'xmm-%s'.format(this.sid); }, getOpkgPackage: function() { return 'xmm-modem'; }, isFloating: function() { return true; }, isVirtual: function() { return true; }, getDevices: function() { return null; }, containsDevice: function(ifname) { return (network.getIfnameOf(ifname) == this.getIfname()); }, renderFormOptions: function(s) { var dev = this.getL3Device() || this.getDevice(), o; o = s.taboption('general', form.Value, 'device', _('Modem port')); o.ucioption = 'device'; o.rmempty = false; o.load = function(section_id) { return callFileList('/dev/').then(L.bind(function(devices) { for (var i = 0; i < devices.length; i++) this.value(devices[i]); return form.Value.prototype.load.apply(this, [section_id]); }, this)); }; o = s.taboption('general', form.Value, 'apn', _('APN')); o.validate = function(section_id, value) { if (value == null || value == '') return true; if (!/^[a-zA-Z0-9\-.]*[a-zA-Z0-9]$/.test(value)) return _('Invalid APN provided'); return true; }; o = s.taboption('general', form.Value, 'pincode', _('PIN')); o.datatype = 'and(uinteger,minlength(4),maxlength(8))'; o = s.taboption('general', form.Value, 'username', _('PAP/CHAP username')); o = s.taboption('general', form.Value, 'password', _('PAP/CHAP password')); o = s.taboption('general', form.ListValue, 'auth', _('Auth Type')); o.value('auto', 'Auto'); o.value('pap', 'PAP'); o.value('chap', 'CHAP'); o.default = 'auto'; o = s.taboption('advanced', form.Value, 'delay', _('Modem init timeout'), _('Maximum amount of seconds to wait for the modem to become ready')); o.placeholder = '10'; o.datatype = 'min(1)'; o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU')); o.placeholder = dev ? (dev.getMTU() || '1500') : '1500'; o.datatype = 'max(9200)'; o = s.taboption('general', form.ListValue, 'pdp', _('PDP Type')); o.value('ipv4v6', 'IPv4/IPv6'); o.value('ip', 'IPv4'); o.value('ipv6', 'IPv6'); o.default = 'ipv4v6'; o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured')); o.default = o.enabled; o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric')); o.placeholder = '0'; o.datatype = 'uinteger'; o.depends('defaultroute', '1'); o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored')); o.default = o.enabled; } }); ================================================ FILE: luci/themes/luci-theme-lightblue/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Bootstrap lightblue Theme LUCI_DEPENDS:= +luci-lua-runtime PKG_LICENSE:=Apache-2.0 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/themes/luci-theme-lightblue/htdocs/luci-static/lightblue/cascade.css ================================================ /*! * LuCI Bootstrap Theme * Copyright 2012 Nut & Bolt * By David Menting * Based on Bootstrap v1.4.0 * * Copyright 2011 Twitter, Inc * Licensed under the Apache License v2.0 * http://www.apache.org/licenses/LICENSE-2.0 * * Designed and built with all the love in the world @twitter by @mdo and @fat. */ /* Reset.less * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ :root { --background-color-delta-l-sign: -1; --background-color-h: 0; --background-color-s: 0%; --background-color-l: 100%; --background-color-high-hsl: var(--background-color-h), var(--background-color-s), var(--background-color-l); --background-color-high: hsl(var(--background-color-high-hsl)); --background-color-medium-hsl: var(--background-color-h), var(--background-color-s), calc(var(--background-color-l) + var(--background-color-delta-l-sign) * calc(6 / 255 * 100%)); --background-color-medium: hsl(var(--background-color-medium-hsl)); --background-color-low-hsl: var(--background-color-h), var(--background-color-s), calc(var(--background-color-l) + var(--background-color-delta-l-sign) * calc(10 / 255 * 100%)); --background-color-low: hsl(var(--background-color-low-hsl)); --text-color-delta-l-sign: 1; --text-color-h: 0; --text-color-s: 0%; --text-color-l: 0%; --text-color-highest-hsl: var(--text-color-h), var(--text-color-s), var(--text-color-l); --text-color-highest: hsl(var(--text-color-highest-hsl)); --text-color-high-hsl: var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) + var(--text-color-delta-l-sign) * calc(64 / 255 * 100%)); --text-color-high: hsl(var(--text-color-high-hsl)); --text-color-medium-hsl: var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) + var(--text-color-delta-l-sign) * calc(128 / 255 * 100%)); --text-color-medium: hsl(var(--text-color-medium-hsl)); --text-color-low-hsl: var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) + var(--text-color-delta-l-sign) * calc(191 / 255 * 100%)); --text-color-low: hsl(var(--text-color-low-hsl)); --border-color-delta-l-sign: -1; --border-color-h: var(--background-color-h); --border-color-s: var(--background-color-s); --border-color-l: var(--background-color-l); --border-color-high-hsl: var(--border-color-h), var(--border-color-s), calc(var(--border-color-l) + var(--border-color-delta-l-sign) * calc(51 / 255 * 100%)); --border-color-high: hsl(var(--border-color-high-hsl)); --border-color-medium-hsl: var(--border-color-h), var(--border-color-s), calc(var(--border-color-l) + var(--border-color-delta-l-sign) * calc(34 / 255 * 100%)); --border-color-medium: hsl(var(--border-color-medium-hsl)); --border-color-low-hsl: var(--border-color-h), var(--border-color-s), calc(var(--border-color-l) + var(--border-color-delta-l-sign) * calc(17 / 255 * 100%)); --border-color-low: hsl(var(--border-color-low-hsl)); --primary-color-high: #1976d2; --primary-color-medium: #1564c0; --primary-color-low: #0d46a1; --on-primary-color: var(--background-color-high); --error-color-high-rgb: 246, 43, 18; --error-color-high: rgb(var(--error-color-high-rgb)); --error-color-medium: #e8210d; --error-color-low: #d00000; --on-error-color: var(--background-color-high); --success-color-high-rgb: 0, 172, 89; --success-color-high: rgb(var(--success-color-high-rgb)); --success-color-medium: #009a4c; --success-color-low: #007936; --on-success-color: var(--background-color-high); --warn-color-high: #efbd0b; --warn-color-medium: #f0c629; --warn-color-low: #f2d24f; --on-warn-color: var(--text-color-highest); --disabled-opacity: .7; color-scheme: light; } :root[data-darkmode="true"] { --background-color-delta-l-sign: 1; --background-color-h: 0; --background-color-s: 0%; --background-color-l: calc(34 / 255 * 100%); --text-color-delta-l-sign: -1; --text-color-h: 0; --text-color-s: 0%; --text-color-l: 100%; --border-color-delta-l-sign: 1; --primary-color-high: #4da1c0; --primary-color-medium: #448da6; --primary-color-low: #3c7a8d; --error-color-high-rgb: 209, 86, 83; --error-color-medium: #bf4e4c; --error-color-low: #b14946; --success-color-high-rgb: 0, 166, 108; --success-color-medium: #00945e; --success-color-low: #008252; --warn-color-high: #a69461; --warn-color-medium: #a68d45; --warn-color-low: #a68732; --on-warn-color: var(--background-color-high); --disabled-opacity: .4; color-scheme: dark; } * { margin: 0; padding: 0; border: 0; box-sizing: border-box; } abbr[title], acronym[title] { border-bottom: 1px dotted; font-weight: inherit; cursor: help; } table { border-collapse: collapse; border-spacing: 0; } ol, ul { list-style: none; } html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } a:focus { outline: none; } a:hover, a:active { outline: none; } footer, header, nav, section { display: block; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } img { -ms-interpolation-mode: bicubic; } button, input, select, option, textarea { font-size: 100%; margin: 0; box-sizing: border-box; vertical-align: baseline; line-height: normal; } button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; word-break: break-all; } button[disabled], input[type="button"][disabled], input[type="reset"][disabled], input[type="submit"][disabled] { opacity: 0.7; } input[type="search"] { -webkit-appearance: textfield; box-sizing: content-box; } input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } textarea { overflow: auto; vertical-align: top; } /* * Scaffolding * Basic and global styles for generating a grid system, structural layout, and page templates * ------------------------------------------------------------------------------------------- */ body { background-color: #fff; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 18px; color: #404040; padding: 18px 5px 5px 5px; margin-top: 40px; } .container { width: 100%; max-width: 940px; margin-left: auto; margin-right: auto; zoom: 1; } .container:before, .container:after { display: table; content: ""; zoom: 1; } .container:after { clear: both; } a { color: #49729e; text-decoration: none; line-height: inherit; font-weight: inherit; } a:hover { color: #B1266e3a; text-decoration: underline; } .pull-right { float: right; } .pull-left { float: left; } .nowrap { white-space: nowrap; } /* Typography.less * Headings, body text, lists, code, and more for a versatile and durable typography system * ---------------------------------------------------------------------------------------- */ p, .cbi-map-descr, .cbi-section-descr, .table .tr.cbi-section-table-descr .th { font-size: 13px; font-weight: normal; line-height: 18px; margin-bottom: 9px; } p small { font-size: 11px; color: #bfbfbf; } h1, h2, h3, legend, h4, h5, h6 { font-weight: bold; color: #49729e; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { color: #bfbfbf; } h1 { margin-bottom: 18px; font-size: 30px; line-height: 36px; } h1 small { font-size: 18px; } h2 { font-size: 24px; line-height: 36px; } h2 small { font-size: 14px; } h3, legend, h4, h5, h6 { line-height: 36px; } h3, legend { font-size: 18px; } h3 small { font-size: 14px; } h4 { font-size: 16px; } h4 small { font-size: 12px; } h5 { font-size: 14px; } h6 { font-size: 13px; color: #bfbfbf; text-transform: uppercase; } ul, ol { margin: 0 0 18px 25px; } ul ul, ul ol, ol ol, ol ul { margin-bottom: 0; } ul { list-style: disc; } ol { list-style: decimal; } li { line-height: 18px; color: #808080; } ul.unstyled { list-style: none; margin-left: 0; } dl { margin-bottom: 18px; } dl dt, dl dd { line-height: 18px; } dl dt { font-weight: bold; } dl dd { margin-left: 9px; } hr { margin: 20px 0 19px; border: 0; border-bottom: 1px solid #eee; } strong { font-style: inherit; font-weight: bold; } em { font-style: italic; font-weight: inherit; line-height: inherit; } small { font-size: 0.9em } address { display: block; line-height: 18px; margin-bottom: 18px; } code, pre { padding: 0 3px 2px; font-family: Monaco, Andale Mono, Courier New, monospace; font-size: 12px; border-radius: 3px; } code { background-color: #6a89a6; color: rgba(0, 0, 0, 0.75); padding: 1px 3px; } pre { background-color: #f5f5f5; display: block; padding: 8.5px; margin: 0 0 18px; line-height: 18px; font-size: 12px; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 3px; white-space: pre; white-space: pre-wrap; word-wrap: break-word; } /* Forms.less * Base styles for various input types, form layouts, and states * ------------------------------------------------------------- */ form { margin-bottom: 18px; } fieldset { margin-bottom: 9px; padding-top: 9px; } fieldset legend { display: block; font-size: 19.5px; line-height: 1; color: #404040; padding-top: 20px; } form .cbi-tab-descr { line-height: 18px; margin-bottom: 18px; } form .clearfix, .cbi-value { margin-bottom: 18px; zoom: 1; } form .clearfix:before, form .clearfix:after, .cbi-value:before, .cbi-value:after { display: table; content: ""; zoom: 1; } form .clearfix:after, .cbi-value:after { clear: both; } label, input, button, select, textarea { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: normal; } form .input, .cbi-value-field { margin-left: 200px; } .cbi-value label.cbi-value-title { padding-top: 6px; font-size: 13px; line-height: 18px; float: left; width: 180px; text-align: right; color: #404040; } input[type=checkbox], input[type=radio] { cursor: pointer; } label > input[type="checkbox"], label > input[type="radio"] { vertical-align: bottom; margin: 0; } input, textarea, select, .cbi-dropdown:not(.btn):not(.cbi-button), .uneditable-input { display: inline-block; width: 210px; padding: 4px; font-size: 13px; line-height: 18px; border: 1px solid #ccc; border-radius: 3px; } input, select, .cbi-dropdown:not(.btn):not(.cbi-button), .uneditable-input { height: 30px; } .uneditable-input { color: #808080; } .cbi-dropdown:not(.btn):not(.cbi-button), .cbi-dynlist { min-width: 210px; max-width: 400px; width: auto; } .cbi-dynlist { height: auto; min-height: 30px; display: inline-flex; flex-direction: column; } .cbi-dynlist > .item { margin-bottom: 4px; box-shadow: 0 0 2px #ccc; background: #fff; padding: 2px 2em 2px 4px; border: 1px solid #ccc; border-radius: 3px; position: relative; pointer-events: none; overflow: hidden; word-break: break-all; } .cbi-dynlist > .item::after { content: "×"; position: absolute; display: inline-flex; align-items: center; top: -1px; right: -1px; bottom: -1px; padding: 0 6px; border: 1px solid #ccc; border-radius: 0 3px 3px 0; font-weight: bold; color: #179; pointer-events: auto; } .cbi-dynlist > .add-item { display: flex; } .cbi-dynlist > .add-item > input, .cbi-dynlist > .add-item > button { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } select { padding: initial; background: #fff; box-shadow: inset 0 -1px 3px rgba(0, 0, 0, 0.1); } input[type=checkbox], input[type=radio] { width: auto; height: auto; padding: 0; margin: 3px 0; *margin-top: 0; /* IE6-7 */ line-height: normal; border: none; } input[type=file] { background-color: #fff; padding: initial; border: initial; line-height: initial; box-shadow: none; width: auto !important; } input[type=button], input[type=reset], input[type=submit] { width: auto; height: auto; } select[multiple] { height: inherit; background-color: #fff; } .td > input[type=text], .td > input[type=password], .td > select, .td > .cbi-dropdown:not(.btn):not(.cbi-button), .cbi-dynlist > .add-item > .cbi-dropdown { width: 100%; } .uneditable-input { background-color: #fff; display: block; border-color: #eee; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); cursor: not-allowed; } ::-moz-placeholder { color: #bfbfbf; } ::-webkit-input-placeholder { color: #bfbfbf; } .item::after, .btn, .cbi-button, input, button, textarea { transition: border linear 0.2s, box-shadow linear 0.2s; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); } .item:hover::after, .btn:hover, .cbi-button:hover, button:hover, input:focus, textarea:focus { outline: 0; border-color: rgba(210, 73, 45, 0.8) !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(210, 73, 45, 0.6); text-decoration: none; } input[type=file]:focus, input[type=checkbox]:focus, select:focus { box-shadow: none; outline: 1px dotted #666; } input[disabled], button[disabled], select[disabled], textarea[disabled], input[readonly], button[readonly], select[readonly], textarea[readonly] { background-color: #f5f5f5; border-color: #ddd; pointer-events: none; cursor: default; } select[readonly], textarea[readonly] { pointer-events: auto; cursor: auto; } .cbi-optionals, .cbi-section-create { padding: 0 0 10px 10px; } .cbi-section-create { margin: -3px; display: inline-flex; align-items: center; } .cbi-section-create > * { margin: 3px; flex: 1 1 auto; } .cbi-section-create > * > input { width: 100%; } .actions, .cbi-page-actions { background: #f5f5f5; margin-bottom: 18px; padding: 17px 20px 18px 17px; border-top: 1px solid #ddd; border-radius: 0 0 3px 3px; text-align: right; } .actions .secondary-action, .cbi-page-actions .secondary-action{ float: right; } .actions .secondary-action a, .cbi-page-actions .secondary-action a { line-height: 30px; } .actions .secondary-action a:hover, .cbi-page-actions .secondary-action a:hover { text-decoration: underline; } .cbi-page-actions > form { display: inline; margin: 0; } .help-inline, .help-block { font-size: 13px; line-height: 18px; color: #bfbfbf; } .help-inline { padding-left: 5px; *position: relative; /* IE6-7 */ *top: -5px; /* IE6-7 */ } .help-block { display: block; max-width: 600px; } /* * Tables.less * Tables for, you guessed it, tabular data * ---------------------------------------- */ .tr { display: table-row; } .table[width="33%"], .th[width="33%"], .td[width="33%"] { width: 33%; } .table[width="100%"], .th[width="100%"], .td[width="100%"] { width: 100%; } .table { display: table; width: 100%; margin-bottom: 18px; padding: 0; font-size: 13px; border-collapse: collapse; position: relative; } .table .th, .table .td { display: table-cell; vertical-align: middle; /* Fixme */ padding: 10px 10px 9px; line-height: 18px; text-align: left; } .table .tr:first-child .th { padding-top: 9px; font-weight: bold; vertical-align: top; } .table .td, .table .th { border-top: 1px solid #ddd; } .tr.placeholder { height: calc(3em + 20px); } .tr.placeholder > .td { position: absolute; left: 0; right: 0; bottom: 0; text-align: center; line-height: 3em; } .tr.drag-over-above, .tr.drag-over-below { border: 2px solid #0069d6; border-width: 2px 0 0 0; } .tr.drag-over-below { border-width: 0 0 2px 0; } /* Patterns.less * Repeatable UI elements outside the base styles provided from the scaffolding * ---------------------------------------------------------------------------- */ header { height: 40px; position: fixed; top: 0; left: 0; right: 0; z-index: 10000; overflow: visible; color: rgb(244,244,244); } header a { color: #49729e; /*text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);*/ } header h3 a:hover, header .brand:hover, header ul .active > a { /*background-color: #333;*/ /*background-color: rgba(255, 255, 255, 0.05);*/ color: #1021b5; text-decoration: none; } header h3 { position: relative; } header h3 a, header .brand { float: left; display: block; padding: 8px 20px 12px; margin-left: 0px; color: #49729e; font-size: 20px; font-family: sans-serif; font-weight: 200; line-height: 1; outline:none; } .brand { display: inline-block; padding: 5px 0 5px 0 !important; width: 135px !important; height: 40px !important; background-image: url('logo.png'); background-size: 135px 30px; background-position: center center; background-repeat: no-repeat; margin-right: 5px; } header p { margin: 0; line-height: 40px; } header .fill { background-color: #f2f2f2; background-repeat: repeat-x; -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); padding: 0 5px; } header div > ul, .nav { display: block; float: left; margin: 0 10px 0 0; position: relative; left: 0; } header div > ul > li, .nav > li { display: block; float: left; } header div > ul a, .nav a { display: block; float: none; padding: 10px 10px 11px; line-height: 19px; text-decoration: none; } header div > ul a:hover, .nav a:hover { color: #1021b5; text-decoration: none; } header div > ul .active > a, .nav .active > a { background-color: #fff; background-color: rgba(0, 0, 0, 0.5); } header div > ul.secondary-nav, .nav.secondary-nav { float: right; margin-left: 10px; margin-right: 0; } header div > ul.secondary-nav .menu-dropdown, .nav.secondary-nav .menu-dropdown, header div > ul.secondary-nav .dropdown-menu, .nav.secondary-nav .dropdown-menu { right: 0; border: 0; } header div > ul a.menu:hover, .nav a.menu:hover, header div > ul li.open .menu, .nav li.open .menu, header div > ul .dropdown-toggle:hover, .nav .dropdown-toggle:hover, header div > ul .dropdown.open .dropdown-toggle, .nav .dropdown.open .dropdown-toggle { background: #444; background: rgba(255, 255, 255, 0.05); } header div > ul .menu-dropdown, .nav .menu-dropdown, header div > ul .dropdown-menu, .nav .dropdown-menu { background-color: #f2f2f2; } header div > ul .menu-dropdown a.menu, .nav .menu-dropdown a.menu, header div > ul .dropdown-menu a.menu, .nav .dropdown-menu a.menu, header div > ul .menu-dropdown .dropdown-toggle, .nav .menu-dropdown .dropdown-toggle, header div > ul .dropdown-menu .dropdown-toggle, .nav .dropdown-menu .dropdown-toggle { color: #f2f2f2; } header div > ul .menu-dropdown a.menu.open, .nav .menu-dropdown a.menu.open, header div > ul .dropdown-menu a.menu.open, .nav .dropdown-menu a.menu.open, header div > ul .menu-dropdown .dropdown-toggle.open, .nav .menu-dropdown .dropdown-toggle.open, header div > ul .dropdown-menu .dropdown-toggle.open, .nav .dropdown-menu .dropdown-toggle.open { background: #444; background: rgba(255, 255, 255, 0.05); } header div > ul .menu-dropdown li a, .nav .menu-dropdown li a, header div > ul .dropdown-menu li a, .nav .dropdown-menu li a { color: #666; text-shadow: none !important; } header div > ul .menu-dropdown li a:hover, .nav .menu-dropdown li a:hover, header div > ul .dropdown-menu li a:hover, .nav .dropdown-menu li a:hover { background-color: #ffffff; background-repeat: repeat-x; color: #1f4287; text-shadow: none !important; } header div > ul .menu-dropdown .active a, .nav .menu-dropdown .active a, header div > ul .dropdown-menu .active a, .nav .dropdown-menu .active a { color: #ffffff; } header div > ul .menu-dropdown .divider, .nav .menu-dropdown .divider, header div > ul .dropdown-menu .divider, .nav .dropdown-menu .divider { background-color: #222; border-color: #444; } header ul .menu-dropdown li a, header ul .dropdown-menu li a { padding: 4px 15px; } li.menu, .dropdown { position: relative; } a.menu:after, .dropdown-toggle:after { width: 0; height: 0; display: inline-block; content: "↓"; text-indent: -99999px; vertical-align: top; margin-top: 8px; margin-left: 4px; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid #1f4287; opacity: 0.5; } .menu-dropdown, .dropdown-menu { background-color: #ffffff; float: left; position: absolute; top: 40px; left: -9999px; z-index: 900; min-width: 160px; max-width: 220px; _width: 160px; margin-left: 0; margin-right: 0; padding: 6px 0; zoom: 1; border-color: #999; border-color: rgba(0, 0, 0, 0.2); border-style: solid; border-width: 0 1px 1px; border-radius: 0 0 6px 6px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); background-clip: padding-box; } .menu-dropdown li, .dropdown-menu li { float: none; display: block; background-color: none; } .menu-dropdown .divider, .dropdown-menu .divider { height: 1px; margin: 5px 0; overflow: hidden; background-color: #eee; border-bottom: 1px solid #ffffff; } header .dropdown-menu a, .dropdown-menu a { display: block; padding: 4px 15px; clear: both; font-weight: normal; line-height: 18px; color: #808080; text-shadow: 0 1px 0 #ffffff; } header .dropdown-menu a:hover, .dropdown-menu a:hover, header .dropdown-menu a.hover, .dropdown-menu a.hover { background-color: #dddddd; background-repeat: repeat-x; background-image: linear-gradient(to bottom, #eee, #ddd); color: #404040; text-decoration: none; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.025), inset 0 -1px rgba(0, 0, 0, 0.025); } .open .menu, .dropdown.open .menu, .open .dropdown-toggle, .dropdown.open .dropdown-toggle { color: #fff; background: #ccc; background: rgba(0, 0, 0, 0.3); } .open .menu-dropdown, .dropdown.open .menu-dropdown, .open .dropdown-menu, .dropdown.open .dropdown-menu { left: 0; } .dropdown:hover ul.dropdown-menu { left: 0; } .dropdown-menu .dropdown-menu { position: absolute; left: 159px; } .dropdown-menu li { position: relative; } .tabs, .cbi-tabmenu { margin: 0 -5px 18px; padding: 0 2px; list-style: none; display: flex; flex-wrap: wrap; background: linear-gradient(#fff 28px, #ddd 28px); background-size: 1px 29px; background-position: left bottom; } .tabs > li, .cbi-tabmenu > li { flex: 0 1 auto; display: flex; align-items: center; height: 25px; max-width: 48%; margin: 4px 2px 0 2px; background: #fff; border: 1px solid #ddd; border-bottom: none; border-radius: 4px 4px 0 0; color: #1f4287; } .tabs > li > a, .cbi-tabmenu > li > a { padding: 4px 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: inherit; text-decoration: none; border-radius: 4px 4px 0 0; line-height: 25px; outline: none; } .tabs > li:not(.active):hover, .cbi-tabmenu > .cbi-tab-disabled:hover { background: linear-gradient(#fff 90%, #ddd 100%); } .tabs > li:not(.active), .cbi-tabmenu > .cbi-tab-disabled { color: #999; background: linear-gradient(#eee 90%, #ddd 100%); } .cbi-tab-disabled[data-errors]::after { content: attr(data-errors); background: #c43c35; color: #fff; min-width: 12px; line-height: 14px; border-radius: 7px; text-align: center; margin: 0 5px 0 0; padding: 1px 2px; } .cbi-tabmenu.map { margin: 0; } .cbi-tabmenu.map > li { font-size: 16.5px; font-weight: bold; } .cbi-tabcontainer > fieldset.cbi-section[id] > legend { display: none; } .tabs .menu-dropdown, .tabs .dropdown-menu { top: 35px; border-width: 1px; border-radius: 0 6px 6px 6px; } .tabs a.menu:after, .tabs .dropdown-toggle:after { border-top-color: #999; margin-top: 15px; margin-left: 5px; } .tabs li.open.menu .menu, .tabs .open.dropdown .dropdown-toggle { border-color: #999; } .tabs li.open a.menu:after, .tabs .dropdown.open .dropdown-toggle:after { border-top-color: #555; } .tab-content > .tab-pane, .tab-content > div { display: none; } .tab-content > .active { display: block; } .breadcrumb { padding: 7px 14px; margin: 0 0 18px; background-color: #f5f5f5; background-repeat: repeat-x; background-image: linear-gradient(to bottom, #fff, #f5f5f5); border: 1px solid #ddd; border-radius: 3px; box-shadow: inset 0 1px 0 #fff; } .breadcrumb li { display: inline; text-shadow: 0 1px 0 #fff; } .breadcrumb .divider { padding: 0 5px; color: #bfbfbf; } .breadcrumb .active a { color: #404040; } footer { margin-top: 17px; padding-top: 17px; border-top: 1px solid #eee; } #modal_overlay { position: fixed; top: 0; bottom: 0; left: -10000px; right: 10000px; background: rgba(0, 0, 0, 0.7); z-index: 900; overflow-y: scroll; -webkit-overflow-scrolling: touch; transition: opacity .125s ease-in; opacity: 0; visibility: hidden; } .modal { width: 90%; margin: 5em auto; display: flex; flex-wrap: wrap; min-height: 32px; max-width: 600px; align-items: center; border-radius: 3px; background: #fff; box-shadow: 0 0 3px #444; padding: 1em 1em .5em 1em; min-width: 270px; } .modal > * { flex-basis: 100%; line-height: normal; margin-bottom: .5em; max-width: 100%; } .modal > pre, .modal > textarea { white-space: pre-wrap; overflow: auto; } body.modal-overlay-active { overflow: hidden; height: 100vh; } body.modal-overlay-active #modal_overlay { left: 0; right: 0; opacity: 1; visibility: visible; } .btn.danger, .alert-message.danger, .btn.danger:hover, .alert-message.danger:hover, .btn.error, .alert-message.error, .btn.error:hover, .alert-message.error:hover, .btn.success, .alert-message.success, .btn.success:hover, .alert-message.success:hover, .btn.info, .alert-message.info, .btn.info:hover, .alert-message.info:hover, .cbi-tooltip.error, .cbi-tooltip.success, .cbi-tooltip.info { color: #fff; } .btn .close, .alert-message .close { font-family: Arial, sans-serif; line-height: 18px; } .modal .btn.danger, .modal .btn { white-space: normal; } .btn.danger, .alert-message.danger, .btn.error, .alert-message.error, .cbi-tooltip.error { background: linear-gradient(to bottom, #1962b0, #1944b0) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .btn.success, .alert-message.success, .cbi-tooltip.success { background: linear-gradient(to bottom, #62c462, #57a957) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .btn.info, .alert-message.info, .cbi-tooltip.info { background: linear-gradient(to bottom, #5bc0de, #339bb9) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .alert-message.notice, .cbi-tooltip.notice { background: linear-gradient(to bottom, #efefef, #fefefe) repeat-x; text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .item::after, .btn, .cbi-button { cursor: pointer; display: inline-block; background: #fff; padding: 5px 14px 6px; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); color: #333; font-size: 13px; line-height: normal; border: 1px solid #ccc; border-bottom-color: #bbb; border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); white-space: pre; } .btn:focus, .cbi-button:focus { outline: 1px dotted #666; } .cbi-input-invalid, .cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button), .cbi-input-invalid.cbi-dropdown:not([open]) > ul > li, .cbi-value-error input { color: #f00; border-color: #f00; } .cbi-button-neutral, .cbi-button-download, .cbi-button-find, .cbi-button-link, .cbi-button-up, .cbi-button-down { color: #444; } .btn.primary, .cbi-button-apply, .cbi-button-negative { border-color: #1f4287; color: #1f4287; text-shadow: none !important; } .cbi-section-remove .cbi-button, .cbi-button-reset { border-color: #179; color: #179; } .cbi-page-actions::after { display: table; content: ""; clear: both; } .cbi-page-actions > * { vertical-align: middle; } .cbi-page-actions > :not([method="post"]):not(.cbi-button-apply):not(.cbi-button-negative):not(.cbi-button-save):not(.cbi-button-reset) { float: left; margin-right: .4em; } .cbi-button-edit, .cbi-button-positive.important, .cbi-button-positive, .cbi-button-action.important, .cbi-button-action, .cbi-button-save, .cbi-button-reload, .cbi-page-actions .cbi-button-apply { color: #FFFFFF; background: #4b9cd1; border-color: #2b73b5; text-shadow: none !important; } .cbi-button-fieldadd, .cbi-section-remove .cbi-button, .cbi-button-remove, .cbi-button-add, .cbi-button-negative.important, .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-reset, .cbi-section-actions .cbi-button-action { color: #132ba1; background: #ffffff; border-color: #132ba1; text-shadow: none !important; } .cbi-page-actions .cbi-button-reset:hover, .cbi-page-actions .cbi-button-save:hover, .cbi-page-actions .cbi-button-apply:hover { color: #FFFFFF; background: #132ba1; border-color: #132ba1; text-shadow: none !important; } .cbi-dropdown { display: inline-flex !important; cursor: pointer; height: auto; position: relative; padding: 0 !important; } .cbi-dropdown:not(.btn):not(.cbi-button) { background: linear-gradient(#fff 0%, #e9e8e6 100%); border: 1px solid #ccc; border-radius: 3px; color: #404040; } .cbi-dynlist > .item:focus, .cbi-dropdown:focus { outline: 2px solid #4b6e9b; } .cbi-dropdown > ul { margin: 0 !important; padding: 0; list-style: none; overflow-x: hidden; overflow-y: auto; display: flex; width: 100%; } .cbi-dropdown.btn > ul:not(.dropdown), .cbi-dropdown.cbi-button > ul:not(.dropdown) { margin: 0 0 0 13px !important; } .cbi-dropdown.btn.spinning > ul:not(.dropdown), .cbi-dropdown.cbi-button.spinning > ul:not(.dropdown) { margin: 0 !important; } .cbi-dropdown > ul.preview { display: none; } .cbi-dropdown > .open, .cbi-dropdown > .more { flex-grow: 0; flex-shrink: 0; display: flex; flex-direction: column; justify-content: center; text-align: center; line-height: 2em; padding: 0 .25em; } .cbi-dropdown.btn > .open, .cbi-dropdown.cbi-button > .open { padding: 0 .5em; margin-left: .5em; border-left: 1px solid; } .cbi-dropdown > .more, .cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder] { color: #777; font-weight: bold; text-shadow: 1px 1px 0px #fff; display: none; justify-content: center; } .cbi-dropdown > ul > li { display: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; flex-grow: 1; align-items: center; align-self: center; color: inherit; } .cbi-dropdown > ul.dropdown > li, .cbi-dropdown:not(.btn):not(.cbi-button) > ul > li { min-height: 20px; padding: .25em; color: #404040; } .cbi-dropdown > ul > li .hide-open { display: block; display: initial; } .cbi-dropdown > ul > li .hide-close { display: none; } .cbi-dropdown > ul > li[display]:not([display="0"]) { border-left: 1px solid #ccc; } .cbi-dropdown[empty] > ul { max-width: 1px; } .cbi-dropdown > ul > li > form { display: none; margin: 0; padding: 0; pointer-events: none; } .cbi-dropdown > ul > li img { vertical-align: middle; margin-right: .25em; } .cbi-dropdown > ul > li > form > input[type="checkbox"] { margin: 0; } .cbi-dropdown > ul > li input[type="text"] { height: 20px; } .cbi-dropdown[open] { position: relative; } .cbi-dropdown[open] > ul.dropdown { display: block; background: #f6f6f5; border: 1px solid #918e8c; box-shadow: 0 0 4px #918e8c; position: absolute; z-index: 1100; max-width: none; min-width: 100%; width: auto; transition: max-height .125s ease-in; } .cbi-dropdown > ul > li[display], .cbi-dropdown[open] > ul.preview, .cbi-dropdown[open] > ul.dropdown > li, .cbi-dropdown[multiple] > ul > li > label, .cbi-dropdown[multiple][open] > ul.dropdown > li, .cbi-dropdown[multiple][more] > .more, .cbi-dropdown[multiple][empty] > .more { flex-grow: 1; display: flex !important; } .cbi-dropdown[empty] > ul > li, .cbi-dropdown[optional][open] > ul.dropdown > li[placeholder], .cbi-dropdown[multiple][open] > ul.dropdown > li > form { display: block !important; } .cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; } .cbi-dropdown[open] > ul.dropdown > li .hide-close { display: block; display: initial; } .cbi-dropdown[open] > ul.dropdown > li { border-bottom: 1px solid #ccc; } .cbi-dropdown[open] > ul.dropdown > li[selected] { background: #0a66a3; } .cbi-dropdown[open] > ul.dropdown > li.focus { background: #6899ba; } .cbi-dropdown[open] > ul.dropdown > li:last-child { margin-bottom: 0; border-bottom: none; } .cbi-dropdown[open] > ul.dropdown > li[unselectable] { opacity: 0.7; } .cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { width: 100%; } .cbi-dropdown[disabled] { pointer-events: none; opacity: .6; } input[type="text"] + .cbi-button, input[type="password"] + .cbi-button, select + .cbi-button { border-radius: 0 3px 3px 0; border-color: #ccc; margin-left: -2px; padding: 0 6px; vertical-align: top; height: 30px; font-size: 14px; line-height: 28px; } /*select + .cbi-button { border-left-color: transparent; }*/ input[type="text"] + .cbi-button-add { color: #fff; border-radius: 0 3px 3px 0; border-color: #2b73b5; background: #2b73b5; } input[type="text"] + .cbi-button-remove { color: #124; border-radius: 0 3px 3px 0; border-color: #ccc; } .cbi-title-ref { color: #234aad; } .cbi-title-ref::after { content: "➙"; } .cbi-tooltip-container { cursor: help; } .cbi-tooltip { position: absolute; z-index: 1000; left: -10000px; box-shadow: 0 0 2px #ccc; border-radius: 3px; background: #fff; white-space: pre; padding: 2px 5px; opacity: 0; transition: opacity .25s ease-in; } .cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { left: auto; opacity: 1; transition: opacity .25s ease-in; } .cbi-progressbar { border: 1px solid var(--border-color-high); border-radius: 3px; position: relative; min-width: 170px; height: 8px; margin: 1.4em 0 4px 0; background: var(--background-color-medium); } .cbi-progressbar > div { background: var(--primary-color-medium); height: 100%; transition: width .25s ease-in; width: 0%; border-radius: 2px; } .cbi-progressbar::before { position: absolute; top: -1.4em; left: 0; content: attr(title); white-space: pre; overflow: hidden; text-overflow: ellipsis; } .zonebadge .cbi-tooltip { padding: 1px; background: inherit; margin: -1.6em 0 0 -5px; border-radius: 3px; pointer-events: none; box-shadow: 0 0 3px #444; } .zonebadge .cbi-tooltip > * { margin: 1px; } .zone-forwards { display: flex; flex-wrap: wrap; } .zone-forwards > * { flex: 1 1 40%; padding: 1px; } .zone-forwards > span { flex-basis: 10%; text-align: center; } .zone-forwards .zone-src, .zone-forwards .zone-dest { display: flex; flex-direction: column; } .btn.active, .btn:active { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn.disabled { cursor: default; opacity: 0.65; box-shadow: none; } .btn[disabled] { cursor: default; opacity: 0.65; box-shadow: none; } .btn.large { font-size: 15px; line-height: normal; padding: 9px 14px 9px; border-radius: 6px; } .btn.small { padding: 7px 9px 7px; font-size: 11px; } button.btn::-moz-focus-inner, input[type=submit].btn::-moz-focus-inner { padding: 0; border: 0; } .close { float: right; color: #000; font-size: 20px; font-weight: bold; line-height: 13.5px; text-shadow: 0 1px 0 #fff; opacity: 0.25; } .close:hover { color: #000; text-decoration: none; opacity: 0.4; } .alert-message { position: relative; padding: .5em .5em .25em .5em; margin-bottom: .5em; color: #404040; background: linear-gradient(to bottom, #fceec1, #eedc94) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); border-width: 1px; border-style: solid; border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); } .alert-message .close { margin-top: 1px; *margin-top: 0; } .alert-message h4, .alert-message h5, .alert-message pre, .alert-message ul, .alert-message li, .alert-message p { color: inherit; border: none; line-height: inherit; background: transparent; padding: 0; margin: .25em 0; } .alert-message button { margin: .25em 0; } .label, header [data-indicator] { padding: 1px 3px 2px; font-size: 9.75px; font-weight: bold; color: #fff !important; text-transform: uppercase; white-space: nowrap; background-color: #216090; border-radius: 3px; text-shadow: none; margin-left: .4em; } header [data-indicator][data-clickable] { cursor: pointer; } a.label:link, a.label:visited { color: #fff; } a.label:hover { text-decoration: none; } .label.important { background-color: #49729e; } .label.warning { background-color: #f89406; } .label.success { background-color: #339933; } .label.notice, header [data-indicator][data-style="active"] { background-color: #6582a1; } /* LuCI specific items */ .hidden { display: none } form.inline { display: inline; margin-bottom: 0; } header .pull-right { padding-top: 8px; } #modemenu li:last-child span.divider { display: none } #syslog { width: 100%; } .cbi-section-table .tr:hover .td, .cbi-section-table .tr:hover .th, .cbi-section-table .tr:hover::before { background-color: #f5f5f5; } .cbi-section-table .tr.cbi-section-table-descr .th { font-weight: normal; } .cbi-section-table-titles.named::before, .cbi-section-table-descr.named::before, .cbi-section-table-row[data-title]::before { content: attr(data-title) " "; display: table-cell; padding: 10px 10px 9px; line-height: 18px; font-weight: bold; vertical-align: middle; } .cbi-section-table-titles.named::before, .cbi-section-table-descr.named::before, .cbi-section-table-row[data-title]::before { border-top: 1px solid #ddd; } .left { text-align: left !important; } .right { text-align: right !important; margin-bottom: 5px !important;} .center { text-align: center !important; } .top { vertical-align: top !important; } .middle { vertical-align: middle !important; } .bottom { vertical-align: bottom !important; } .cbi-value-field { line-height: 1.5em; } .cbi-value-field input[type=checkbox], .cbi-value-field input[type=radio] { margin-top: 8px; margin-right: 6px; } table table td, .cbi-value-field table td { border: none; } .table.cbi-section-table input[type="password"], .table.cbi-section-table input[type="text"], .table.cbi-section-table textarea, .table.cbi-section-table select { width: 100%; } .table.cbi-section-table .td.cbi-section-table-cell { white-space: nowrap; text-align: right; } .table.cbi-section-table .td.cbi-section-table-cell select { width: inherit; } .td.cbi-section-actions { text-align: right; vertical-align: middle; } .td.cbi-section-actions > * { display: flex; } .td.cbi-section-actions > * > *, .td.cbi-section-actions > * > form > * { flex: 1 1 4em; margin: 0 1px; } .td.cbi-section-actions > * > form { display: inline-flex; margin: 0; } .table.valign-middle .td { vertical-align: middle; } .cbi-rowstyle-2, .tr.table-titles, .tr.cbi-section-table-titles { background: #f9f9f9; } .cbi-value-description { background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgdmlld0JveD0nMCAwIDE2IDE2Jz48Y2lyY2xlIGN4PSc4JyBjeT0nOCcgcj0nNy41JyBmaWxsPScjNGI5Y2QxJyBzdHJva2U9JyMyYjczYjUnLz48dGV4dCB4PSc4JyB5PScxMicgZm9udC1zaXplPScxMScgZm9udC1mYW1pbHk9J3NhbnMtc2VyaWYnIGZvbnQtd2VpZ2h0PSdib2xkJyB0ZXh0LWFuY2hvcj0nbWlkZGxlJyBmaWxsPScjZmZmJz4/PC90ZXh0Pjwvc3ZnPg=="); background-position: .25em .2em; background-repeat: no-repeat; margin: .25em 0 0 0; padding: 0 0 0 1.7em; } .cbi-section-error { border: 1px solid #f00; border-radius: 3px; background-color: #a2c7eb; padding: 5px; margin-bottom: 18px; } .cbi-section-error ul { margin: 0 0 0 20px; } .cbi-section-error ul li { color: #f00; font-weight: bold; } .ifacebox { background-color: #fff; border: 1px solid #ccc; margin: 0 10px; text-align: center; white-space: nowrap; background-image: linear-gradient(#fff, #fff 25%, #f9f9f9); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); display: inline-flex; flex-direction: column; line-height: 1.2em; min-width: 100px; } .ifacebox .ifacebox-head { border-bottom: 1px solid #ccc; padding: 2px; background: #eee; } .ifacebox .ifacebox-head.active { background: #4b9cd1; } .ifacebox .ifacebox-body { padding: .25em; } .ifacebadge { display: inline-block; flex-direction: row; white-space: nowrap; background-color: #fff; border: 1px solid #ccc; padding: 2px; background-image: linear-gradient(#fff, #fff 25%, #f9f9f9); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: default; line-height: 1.2em; } .ifacebadge img { width: 16px; height: 16px; vertical-align: middle; } .ifacebadge-active { border-color: #000; font-weight: bold; } .network-status-table { display: flex; flex-wrap: wrap; } .network-status-table .ifacebox { margin: .5em; flex-grow: 1; } .network-status-table .ifacebox-body { display: flex; flex-direction: column; height: 100%; text-align: left; } .network-status-table .ifacebox-body > * { margin: .25em; } .network-status-table .ifacebox-body > span { flex: 10 10 auto; height: 100%; } .network-status-table .ifacebox-body > div { margin: -.125em; display: flex; flex-wrap: wrap; } #dsl_status_table .ifacebox-body span > strong { display: inline-block; min-width: 35%; } .ifacebadge.large, .network-status-table .ifacebox-body .ifacebadge { display: flex; flex: 1; padding: .25em; min-width: 220px; margin: .125em; } .ifacebadge.large { display: inline-flex; } .network-status-table .ifacebox-body .ifacebadge > span { overflow: hidden; text-overflow: ellipsis; } .ifacebadge > *, .ifacebadge.large > * { margin: 0 .125em; } .zonebadge { padding: 2px; border-radius: 4px; display: inline-block; white-space: nowrap; color: #666; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); } .zonebadge > em, .zonebadge > strong { margin: 0 2px; display: inline-block; } .zonebadge input { width: 6em; } .zonebadge > .ifacebadge { margin-left: 2px; } .zonebadge-empty { border: 1px dashed #aaa; color: #aaa; font-style: italic; font-size: smaller; } div.cbi-value var, .td.cbi-value-field var { font-style: italic; color: #0069d6; } div.cbi-value var[data-tooltip], .td.cbi-value-field var[data-tooltip], div.cbi-value var.cbi-tooltip-container, .td.cbi-value-field var.cbi-tooltip-container { cursor: help; border-bottom: 1px dotted #0069d6; } div.cbi-value var.cbi-tooltip-container, .td.cbi-value-field var.cbi-tooltip-container .cbi-tooltip { font-style: normal; white-space: normal; color: #444; } #modal_overlay > .modal.uci-dialog, #modal_overlay > .modal.cbi-modal { max-width: 900px; } .uci-change-list { line-height: 170%; white-space: pre; } .uci-change-list del, .uci-change-list ins, .uci-change-list var, .uci-change-legend-label del, .uci-change-legend-label ins, .uci-change-legend-label var { text-decoration: none; font-family: monospace; font-style: normal; border: 1px solid #ccc; background: #eee; padding: 2px; display: block; line-height: 15px; margin-bottom: 1px; } .uci-change-list ins, .uci-change-legend-label ins { border-color: #0f0; background: #cfc; } .uci-change-list del, .uci-change-legend-label del { border-color: #f00; background: #fcc; } .uci-change-list var, .uci-change-legend-label var { border-color: #ccc; background: #eee; } .uci-change-list var ins, .uci-change-list var del { display: inline-block; border: none; width: 100%; padding: 0; } .uci-change-legend { padding: 5px; } .uci-change-legend-label { width: 150px; float: left; } .uci-change-legend-label > ins, .uci-change-legend-label > del, .uci-change-legend-label > var { float: left; margin-right: 4px; width: 16px; height: 16px; display: block; position: relative; } .uci-change-legend-label var ins, .uci-change-legend-label var del { border: none; position: absolute; top: 2px; left: 2px; right: 2px; bottom: 2px; } #modal_overlay { position: fixed; top: 0; bottom: 0; left: -10000px; right: 10000px; background: rgba(0, 0, 0, 0.7); z-index: 900; overflow-y: scroll; -webkit-overflow-scrolling: touch; transition: opacity .125s ease-in; opacity: 0; } #modal_overlay > .modal { width: 90%; margin: 5em auto; display: flex; flex-wrap: wrap; min-height: 32px; max-width: 600px; align-items: center; border-radius: 3px; background: #fff; box-shadow: 0 0 3px #444; padding: 1em 1em .5em 1em; min-width: 270px; } #modal_overlay .modal > * { flex-basis: 100%; line-height: normal; margin-bottom: .5em; } #modal_overlay .modal > pre, #modal_overlay .modal > textarea { white-space: pre-wrap; overflow: auto; } body.modal-overlay-active { overflow: hidden; height: 100vh; } body.modal-overlay-active #modal_overlay { left: 0; right: 0; opacity: 1; } html body.apply-overlay-active { height: calc(100vh - 63px); } #applyreboot-section { line-height: 300%; } [data-page="admin-network-dhcp"] [data-name="ip"] { width: 15%; } @keyframes flash { 0% { opacity: 1; } 50% { opacity: .5; } 100% { opacity: 1; } } .flash { animation: flash .35s; } .spinning { position: relative; padding-left: 32px !important; } .spinning::before { position: absolute; top: 0; left: 0; bottom: 0; width: 32px; content: " "; background: url(../resources/icons/loading.svg) no-repeat center; background-size: 16px; } [data-tab-title] { height: 0; opacity: 0; overflow: hidden; } [data-tab-active="true"] { opacity: 1; height: auto; overflow: visible; transition: opacity .25s ease-in; } .cbi-filebrowser { min-width: 210px; max-width: 100%; border: 1px solid #ccc; border-radius: 3px; display: flex; flex-direction: column; opacity: 0; height: 0; overflow: hidden; } .cbi-filebrowser.open { opacity: 1; height: auto; overflow: visible; transition: opacity .25s ease-in; } .cbi-filebrowser > * { max-width: 100%; overflow: hidden; text-overflow: ellipsis; padding: 0 0 .25em 0; margin: .25em .25em 0px .25em; white-space: nowrap; border-bottom: 1px solid #ccc; } .cbi-filebrowser .cbi-button-positive { margin-right: .25em; } .cbi-filebrowser > div { border-bottom: none; } .cbi-filebrowser > ul > li { display: flex; flex-direction: row; } .cbi-filebrowser > ul > li:hover { background: #f5f5f5; } .cbi-filebrowser > ul > li > div:first-child { flex: 10; overflow: hidden; text-overflow: ellipsis; } .cbi-filebrowser > ul > li > div:last-child { flex: 3; text-align: right; } .cbi-filebrowser > ul > li > div:last-child > button { padding: .125em .25em; margin: 1px 0 1px .25em; } .cbi-filebrowser .upload { display: flex; flex-direction: row; flex-wrap: wrap; margin: 0 -.125em .25em -.125em; padding: 0 0 .125em 0px; border-bottom: 1px solid #ccc; } .cbi-filebrowser .upload > * { margin: .125em; flex: 1; } .cbi-filebrowser .upload > .btn { flex-basis: 60px; } .cbi-filebrowser .upload > div { flex: 10; min-width: 150px; } .cbi-filebrowser .upload > div > input { width: 100%; } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } .fade-in { animation: fade-in .4s ease; } .fade-out { animation: fade-out .4s ease; } .assoclist .ifacebadge { display: flex; flex-direction: column; align-items: center; white-space: normal; text-align: center; } .assoclist .ifacebadge > img { margin: .2em; } .assoclist .td:nth-of-type(3), .assoclist .td:nth-of-type(5) { width: 25%; } .assoclist .td:nth-of-type(6) button { word-break: normal; } ================================================ FILE: luci/themes/luci-theme-lightblue/htdocs/luci-static/lightblue/mobile.css ================================================ header h3 a, header .brand { display:none !important; } @media screen and (max-device-width: 600px) { #maincontent.container { margin-top: 30px; } .tabs, .cbi-tabmenu { background: linear-gradient(#fff 20%, #ddd 100%); background-size: 1px 34px; margin-bottom: 10px; } .tabs > li, .cbi-tabmenu > li { height: 30px; } .tabs > li > a, .cbi-tabmenu > li > a { padding: 0 8px; line-height: 30px; } .table { display: flex; flex-direction: column; width: 100%; } .tr { display: flex; flex-direction: row; flex-wrap: wrap; align-items: flex-end; border-top: 1px solid #ddd; padding: 5px 0; margin: 0 -3px; } .table .th, .table .td, .table .tr::before { flex: 2 2 33%; align-self: flex-start; overflow: hidden; text-overflow: ellipsis; word-wrap: break-word; display: inline-block; border-top: none; padding: 3px; box-sizing: border-box; } .table .td.cbi-dropdown-open { overflow: visible; } .col-1 { flex: 1 1 30px !important; -webkit-flex: 1 1 30px !important; } .col-2 { flex: 2 2 60px !important; -webkit-flex: 2 2 60px !important; } .col-3 { flex: 3 3 90px !important; -webkit-flex: 3 3 90px !important; } .col-4 { flex: 4 4 120px !important; -webkit-flex: 4 4 120px !important; } .col-5 { flex: 5 5 150px !important; -webkit-flex: 5 5 150px !important; } .col-6 { flex: 6 6 180px !important; -webkit-flex: 6 6 180px !important; } .col-7 { flex: 7 7 210px !important; -webkit-flex: 7 7 210px !important; } .col-8 { flex: 8 8 240px !important; -webkit-flex: 8 8 240px !important; } .col-9 { flex: 9 9 270px !important; -webkit-flex: 9 9 270px !important; } .col-10 { flex: 10 10 300px !important; -webkit-flex: 10 10 300px !important; } .td select { word-wrap: normal; } .td[data-widget="button"], .td[data-widget="fvalue"] { flex: 1 1 17%; text-align: left; } .td.cbi-value-field { align-self: flex-start; } .td.cbi-value-field .cbi-button { width: 100%; } .table.cbi-section-table { border: none; background: none; margin: 0; } .tr.table-titles, .cbi-section-table-titles, .cbi-section-table-descr { display: none; } .cbi-section-table-row { display: flex; flex-direction: row; flex-wrap: wrap; margin: 0 0 .5em 0; } .cbi-section-table + .cbi-section-create { padding-top: 0; } .tr[data-title]::before { display: block; flex: 1 1 100%; background: #f5f5f5 !important; font-size: 16px; border-bottom: 1px solid #ddd; } .td[data-title]::before, .td[data-description]::after { display: block; } .td[data-title] ~ .td.cbi-section-actions { align-self: flex-start; } .td[data-title] ~ .td.cbi-section-actions::before { display: block; content: "\a0"; } .td.cbi-section-actions { overflow: initial; max-width: 100%; padding: 3px 2px; } .hide-sm, .hide-xs { display: none !important; } .td.cbi-value-field { flex-basis: 100%; } .td.cbi-value-field[data-widget="dvalue"] { flex-basis: 50%; } .td.cbi-value-field[data-widget="button"], .td.cbi-value-field[data-widget="fvalue"] { flex-basis: 25%; text-align: left; } .cbi-section-table .tr:hover .td, .cbi-section-table .tr:hover .th, .cbi-section-table .tr:hover::before { background-color: transparent; } .cbi-value { padding-bottom: .5em; border-bottom: 1px solid #ddd; margin-bottom: .5em; } .cbi-value label.cbi-value-title { float: none; font-weight: bold; } .cbi-value-field, .cbi-dropdown { width: 100%; margin: 0; } input, textarea, select, .cbi-dropdown > ul > li input[type="text"] { font-size: 16px !important; line-height: 28px; height: auto; } select, input[type="text"], input[type="password"] { width: 100%; height: 30px; } input.cbi-input-password { width: calc(100% - 25px); } [data-dynlist] { display: block; } [data-dynlist] > .add-item > input { width: calc(100% - 21px); } [data-dynlist] > .add-item > .cbi-button { margin-right: -1px; } input[type="text"] + .cbi-button, input[type="password"] + .cbi-button, select + .cbi-button { font-size: 14px !important; line-height: 28px; height: 30px; box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; } .cbi-value-field input[type="checkbox"], .cbi-value-field input[type="radio"] { margin: 0; } .btn, .cbi-button { font-size: 14px !important; padding: 4px 8px; } .actions, .cbi-page-actions { border-top: none; margin-top: -.5em; padding: 8px; } [data-page="admin-status-overview"] .cbi-section:nth-of-type(1) .td:first-child, [data-page="admin-status-overview"] .cbi-section:nth-of-type(2) .td:first-child { flex-grow: 1; } header .pull-right .label { white-space: normal; display: inline-block; text-align: center; line-height: 12px; margin: 1px 0; } header > .fill { padding: 1px; } header > .fill > .container { display: flex; flex-direction: row; } header .nav { flex: 3 3 80%; margin: 2px 5px 2px 0; display: flex; flex-wrap: wrap; justify-content: flex-start; } header .nav a { padding: 2px 6px; } header .pull-right { flex: 1 1 20%; display: flex; flex-direction: column; padding: 0; justify-content: space-around; } .menu-dropdown, .dropdown-menu { top: 23px; } body { padding-top: 30px; } .cbi-optionals, .cbi-section-create { padding: 0 0 14px 0; } #cbi-network-switch_vlan .th, #cbi-network-switch_vlan .td { flex-basis: 12%; } #cbi-network-switch_vlan .td.cbi-section-actions { flex-basis: 100%; } #cbi-network-switch_vlan .td.cbi-section-actions::before { display: none; } #cbi-network-switch_vlan .td.cbi-section-actions > * { width: auto; display: block; } #wifi_assoclist_table .td, [data-page="admin-status-processes"] .td { flex-basis: 50% !important; } [data-page="admin-status-processes"] .td[data-widget="button"] { flex-basis: 33% !important; } [data-page="admin-status-processes"] .td[data-name="PID"], [data-page="admin-status-processes"] .td[data-name="USER"] { flex-basis: 25% !important; } [data-page="admin-system-fstab"] .td[data-widget="button"]::before, [data-page="admin-system-startup"] .td[data-widget="button"]::before, [data-page="admin-status-processes"] .td[data-widget="button"]::before { display: none; } } @media screen and (max-device-width: 375px) { #maincontent.container { margin-top: 55px; } .cbi-page-actions { display: flex; flex-wrap: wrap; justify-content: space-between; margin: 0 -1px; padding: 0; } .cbi-page-actions .cbi-button:not(.cbi-dropdown) { flex: 1 1 calc(50% - 2px); margin: 1px !important; overflow: hidden; text-overflow: ellipsis; } .cbi-page-actions .cbi-button-negative, .cbi-page-actions .cbi-button-primary, .cbi-page-actions .cbi-button-apply { flex-basis: calc(100% - -2px); } .cbi-section-actions .cbi-button { overflow: hidden; text-overflow: ellipsis; } body[data-page="admin-network-wireless"] .td[data-name="_badge"] { max-width: 50px; align-self: center; } body[data-page="admin-network-wireless"] .td[data-name="_badge"] .ifacebadge { display: flex; align-items: center; flex-direction: column; } body[data-page="admin-network-wireless"] .td[data-name="_stat"] { flex-basis: 60%; } body[data-page="admin-network-network"] .td.cbi-section-actions::before, body[data-page="admin-network-wireless"] .td.cbi-section-actions::before { content: none !important; } } @media screen and (max-device-width: 200px) { #maincontent.container { margin-top: 230px; } } @media screen and (max-width: 375px) { .td .ifacebox { width: 100%; margin: 0 !important; flex-direction: row; } .td .ifacebox .ifacebox-head { min-width: 25%; justify-content: space-around; } .td .ifacebox .ifacebox-head, .td .ifacebox .ifacebox-body { display: flex; border-bottom: none; align-items: center; } .td .ifacebox .ifacebox-head > *, .ifacebox .ifacebox-body > * { margin: .125em; } } ================================================ FILE: luci/themes/luci-theme-lightblue/htdocs/luci-static/resources/menu-lightblue.js ================================================ 'use strict'; 'require baseclass'; 'require ui'; return baseclass.extend({ __init__: function() { ui.menu.load().then(L.bind(this.render, this)); }, render: function(tree) { var node = tree, url = ''; this.renderModeMenu(tree); if (L.env.dispatchpath.length >= 3) { for (var i = 0; i < 3 && node; i++) { node = node.children[L.env.dispatchpath[i]]; url = url + (url ? '/' : '') + L.env.dispatchpath[i]; } if (node) this.renderTabMenu(node, url); } document.addEventListener('poll-start', this.handleBodyMargin); document.addEventListener('poll-stop', this.handleBodyMargin); document.addEventListener('uci-new-changes', this.handleBodyMargin); document.addEventListener('uci-clear-changes', this.handleBodyMargin); window.addEventListener('resize', this.handleBodyMargin); this.handleBodyMargin(); }, renderTabMenu: function(tree, url, level) { var container = document.querySelector('#tabmenu'), ul = E('ul', { 'class': 'tabs' }), children = ui.menu.getChildren(tree), activeNode = null; for (var i = 0; i < children.length; i++) { var isActive = (L.env.dispatchpath[3 + (level || 0)] == children[i].name), activeClass = isActive ? ' active' : '', className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass); ul.appendChild(E('li', { 'class': className }, [ E('a', { 'href': L.url(url, children[i].name) }, [ _(children[i].title) ] )])); if (isActive) activeNode = children[i]; } if (ul.children.length == 0) return E([]); container.appendChild(ul); container.style.display = ''; if (activeNode) this.renderTabMenu(activeNode, url + '/' + activeNode.name, (level || 0) + 1); return ul; }, renderMainMenu: function(tree, url, level) { var ul = level ? E('ul', { 'class': 'dropdown-menu' }) : document.querySelector('#topmenu'), children = ui.menu.getChildren(tree); if (children.length == 0 || level > 1) return E([]); for (var i = 0; i < children.length; i++) { var submenu = this.renderMainMenu(children[i], url + '/' + children[i].name, (level || 0) + 1), subclass = (!level && submenu.firstElementChild) ? 'dropdown' : null, linkclass = (!level && submenu.firstElementChild) ? 'menu' : null, linkurl = submenu.firstElementChild ? '#' : L.url(url, children[i].name); var li = E('li', { 'class': subclass }, [ E('a', { 'class': linkclass, 'href': linkurl }, [ _(children[i].title) ]), submenu ]); ul.appendChild(li); } ul.style.display = ''; return ul; }, renderModeMenu: function(tree) { var ul = document.querySelector('#modemenu'), children = ui.menu.getChildren(tree); for (var i = 0; i < children.length; i++) { var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0); ul.appendChild(E('li', { 'class': isActive ? 'active' : null }, [ E('a', { 'href': L.url(children[i].name) }, [ _(children[i].title) ]), ' ', E('span', { 'class': 'divider' }, [ '|' ]) ])); if (isActive) this.renderMainMenu(children[i], children[i].name); } if (ul.children.length > 1) ul.style.display = ''; }, handleBodyMargin: function(ev) { var body = document.querySelector('body'), head = document.querySelector('header'); body.style.marginTop = head.offsetHeight + 'px'; } }); ================================================ FILE: luci/themes/luci-theme-lightblue/luasrc/view/themes/lightblue/footer.htm ================================================ <%# Copyright 2008 Steven Barth Copyright 2008 Jo-Philipp Wich Copyright 2012 David Menting Licensed to the public under the Apache License 2.0. -%> <% local ver = require "luci.version" %>
<%= ver.distversion %> © 132lan.ru <%= os.date("%Y") %> 
================================================ FILE: luci/themes/luci-theme-lightblue/luasrc/view/themes/lightblue/header.htm ================================================ <%# Copyright 2008 Steven Barth Copyright 2008-2016 Jo-Philipp Wich Copyright 2012 David Menting Licensed to the public under the Apache License 2.0. -%> <% local sys = require "luci.sys" local util = require "luci.util" local http = require "luci.http" local disp = require "luci.dispatcher" local boardinfo = util.ubus("system", "board") local node = disp.context.dispatched -- send as HTML5 http.prepare_content("text/html") -%> <%=striptags( (boardinfo.hostname or "?") .. ( (node and node.title) and ' - ' .. translate(node.title) or '')) %> - LuCI <% if node and node.css then %> <% end -%> <% if css then %> <% end -%> ">
<%- if luci.sys.process.info("uid") == 0 and luci.sys.user.getuser("root") and not luci.sys.user.getpasswd("root") then -%>

<%:No password set!%>

<%:There is no password set on this router. Please configure a root password to protect the web interface.%>

<% if disp.lookup("admin/system/admin") then %> <% end %>
<%- end -%> ================================================ FILE: luci/themes/luci-theme-lightblue/root/etc/uci-defaults/30_luci-theme-lightblue ================================================ #!/bin/sh if [ "$PKG_UPGRADE" != 1 ]; then uci get luci.themes.Lightblue >/dev/null 2>&1 || \ uci batch <<-EOF set luci.themes.Lightblue=/luci-static/lightblue set luci.main.mediaurlbase=/luci-static/lightblue commit luci EOF fi exit 0 ================================================ FILE: luci/themes/luci-theme-merona/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Bootstrap Merona Theme LUCI_DEPENDS:= +luci-lua-runtime PKG_LICENSE:=Apache-2.0 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/themes/luci-theme-merona/htdocs/luci-static/merona/cascade.css ================================================ /*! * LuCI Bootstrap Theme * Copyright 2012 Nut & Bolt * By David Menting * Based on Bootstrap v1.4.0 * * Copyright 2011 Twitter, Inc * Licensed under the Apache License v2.0 * http://www.apache.org/licenses/LICENSE-2.0 * * Designed and built with all the love in the world @twitter by @mdo and @fat. */ /* Reset.less * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ :root { --background-color-delta-l-sign: -1; --background-color-h: 0; --background-color-s: 0%; --background-color-l: 100%; --background-color-high-hsl: var(--background-color-h), var(--background-color-s), var(--background-color-l); --background-color-high: hsl(var(--background-color-high-hsl)); --background-color-medium-hsl: var(--background-color-h), var(--background-color-s), calc(var(--background-color-l) + var(--background-color-delta-l-sign) * calc(6 / 255 * 100%)); --background-color-medium: hsl(var(--background-color-medium-hsl)); --background-color-low-hsl: var(--background-color-h), var(--background-color-s), calc(var(--background-color-l) + var(--background-color-delta-l-sign) * calc(10 / 255 * 100%)); --background-color-low: hsl(var(--background-color-low-hsl)); --text-color-delta-l-sign: 1; --text-color-h: 0; --text-color-s: 0%; --text-color-l: 0%; --text-color-highest-hsl: var(--text-color-h), var(--text-color-s), var(--text-color-l); --text-color-highest: hsl(var(--text-color-highest-hsl)); --text-color-high-hsl: var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) + var(--text-color-delta-l-sign) * calc(64 / 255 * 100%)); --text-color-high: hsl(var(--text-color-high-hsl)); --text-color-medium-hsl: var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) + var(--text-color-delta-l-sign) * calc(128 / 255 * 100%)); --text-color-medium: hsl(var(--text-color-medium-hsl)); --text-color-low-hsl: var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) + var(--text-color-delta-l-sign) * calc(191 / 255 * 100%)); --text-color-low: hsl(var(--text-color-low-hsl)); --border-color-delta-l-sign: -1; --border-color-h: var(--background-color-h); --border-color-s: var(--background-color-s); --border-color-l: var(--background-color-l); --border-color-high-hsl: var(--border-color-h), var(--border-color-s), calc(var(--border-color-l) + var(--border-color-delta-l-sign) * calc(51 / 255 * 100%)); --border-color-high: hsl(var(--border-color-high-hsl)); --border-color-medium-hsl: var(--border-color-h), var(--border-color-s), calc(var(--border-color-l) + var(--border-color-delta-l-sign) * calc(34 / 255 * 100%)); --border-color-medium: hsl(var(--border-color-medium-hsl)); --border-color-low-hsl: var(--border-color-h), var(--border-color-s), calc(var(--border-color-l) + var(--border-color-delta-l-sign) * calc(17 / 255 * 100%)); --border-color-low: hsl(var(--border-color-low-hsl)); /* ИЗМЕНЕНИЕ: Заменяем голубые цвета на зеленые */ --primary-color-high: #2e7d32; --primary-color-medium: #388e3c; --primary-color-low: #1b5e20; --on-primary-color: var(--background-color-high); --error-color-high-rgb: 246, 43, 18; --error-color-high: rgb(var(--error-color-high-rgb)); --error-color-medium: #e8210d; --error-color-low: #d00000; --on-error-color: var(--background-color-high); --success-color-high-rgb: 76, 175, 80; --success-color-high: rgb(var(--success-color-high-rgb)); --success-color-medium: #4caf50; --success-color-low: #388e3c; --on-success-color: var(--background-color-high); --warn-color-high: #ff9800; --warn-color-medium: #ffa726; --warn-color-low: #ffb74d; --on-warn-color: var(--text-color-highest); --disabled-opacity: .7; color-scheme: light; } :root[data-darkmode="true"] { --background-color-delta-l-sign: 1; --background-color-h: 0; --background-color-s: 0%; --background-color-l: calc(34 / 255 * 100%); --text-color-delta-l-sign: -1; --text-color-h: 0; --text-color-s: 0%; --text-color-l: 100%; --border-color-delta-l-sign: 1; /* ИЗМЕНЕНИЕ: Темно-зеленые тона для темной темы */ --primary-color-high: #4caf50; --primary-color-medium: #388e3c; --primary-color-low: #2e7d32; --error-color-high-rgb: 209, 86, 83; --error-color-medium: #bf4e4c; --error-color-low: #b14946; --success-color-high-rgb: 76, 175, 80; --success-color-medium: #4caf50; --success-color-low: #388e3c; --warn-color-high: #ff9800; --warn-color-medium: #f57c00; --warn-color-low: #ef6c00; --on-warn-color: var(--background-color-high); --disabled-opacity: .4; color-scheme: dark; } * { margin: 0; padding: 0; border: 0; box-sizing: border-box; } abbr[title], acronym[title] { border-bottom: 1px dotted; font-weight: inherit; cursor: help; } table { border-collapse: collapse; border-spacing: 0; } ol, ul { list-style: none; } html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } a:focus { outline: none; } a:hover, a:active { outline: none; } footer, header, nav, section { display: block; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } img { -ms-interpolation-mode: bicubic; } button, input, select, option, textarea { font-size: 100%; margin: 0; box-sizing: border-box; vertical-align: baseline; line-height: normal; } button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; word-break: break-all; } button[disabled], input[type="button"][disabled], input[type="reset"][disabled], input[type="submit"][disabled] { opacity: 0.7; } input[type="search"] { -webkit-appearance: textfield; box-sizing: content-box; } input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } textarea { overflow: auto; vertical-align: top; } /* * Scaffolding * Basic and global styles for generating a grid system, structural layout, and page templates * ------------------------------------------------------------------------------------------- */ body { background-color: #fff; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 18px; color: #404040; padding: 18px 5px 5px 5px; margin-top: 40px; } .container { width: 100%; max-width: 940px; margin-left: auto; margin-right: auto; zoom: 1; } .container:before, .container:after { display: table; content: ""; zoom: 1; } .container:after { clear: both; } /* ИЗМЕНЕНИЕ: Цвет ссылок на зеленый */ a { color: #2e7d32; text-decoration: none; line-height: inherit; font-weight: inherit; } a:hover { color: #1b5e20; text-decoration: underline; } .pull-right { float: right; } .pull-left { float: left; } .nowrap { white-space: nowrap; } /* Typography.less * Headings, body text, lists, code, and more for a versatile and durable typography system * ---------------------------------------------------------------------------------------- */ p, .cbi-map-descr, .cbi-section-descr, .table .tr.cbi-section-table-descr .th { font-size: 13px; font-weight: normal; line-height: 18px; margin-bottom: 9px; } p small { font-size: 11px; color: #bfbfbf; } /* ИЗМЕНЕНИЕ: Цвет заголовков на зеленый */ h1, h2, h3, legend, h4, h5, h6 { font-weight: bold; color: #2e7d32; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { color: #bfbfbf; } h1 { margin-bottom: 18px; font-size: 30px; line-height: 36px; } h1 small { font-size: 18px; } h2 { font-size: 24px; line-height: 36px; } h2 small { font-size: 14px; } h3, legend, h4, h5, h6 { line-height: 36px; } h3, legend { font-size: 18px; } h3 small { font-size: 14px; } h4 { font-size: 16px; } h4 small { font-size: 12px; } h5 { font-size: 14px; } h6 { font-size: 13px; color: #bfbfbf; text-transform: uppercase; } ul, ol { margin: 0 0 18px 25px; } ul ul, ul ol, ol ol, ol ul { margin-bottom: 0; } ul { list-style: disc; } ol { list-style: decimal; } li { line-height: 18px; color: #808080; } ul.unstyled { list-style: none; margin-left: 0; } dl { margin-bottom: 18px; } dl dt, dl dd { line-height: 18px; } dl dt { font-weight: bold; } dl dd { margin-left: 9px; } hr { margin: 20px 0 19px; border: 0; border-bottom: 1px solid #eee; } strong { font-style: inherit; font-weight: bold; } em { font-style: italic; font-weight: inherit; line-height: inherit; } small { font-size: 0.9em } address { display: block; line-height: 18px; margin-bottom: 18px; } code, pre { padding: 0 3px 2px; font-family: Monaco, Andale Mono, Courier New, monospace; font-size: 12px; border-radius: 3px; } code { background-color: #81c784; color: rgba(0, 0, 0, 0.75); padding: 1px 3px; } pre { background-color: #f5f5f5; display: block; padding: 8.5px; margin: 0 0 18px; line-height: 18px; font-size: 12px; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 3px; white-space: pre; white-space: pre-wrap; word-wrap: break-word; } /* Forms.less * Base styles for various input types, form layouts, and states * ------------------------------------------------------------- */ form { margin-bottom: 18px; } fieldset { margin-bottom: 9px; padding-top: 9px; } fieldset legend { display: block; font-size: 19.5px; line-height: 1; color: #404040; padding-top: 20px; } form .cbi-tab-descr { line-height: 18px; margin-bottom: 18px; } form .clearfix, .cbi-value { margin-bottom: 18px; zoom: 1; } form .clearfix:before, form .clearfix:after, .cbi-value:before, .cbi-value:after { display: table; content: ""; zoom: 1; } form .clearfix:after, .cbi-value:after { clear: both; } label, input, button, select, textarea { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: normal; } form .input, .cbi-value-field { margin-left: 200px; } .cbi-value label.cbi-value-title { padding-top: 6px; font-size: 13px; line-height: 18px; float: left; width: 180px; text-align: right; color: #404040; } input[type=checkbox], input[type=radio] { cursor: pointer; } label > input[type="checkbox"], label > input[type="radio"] { vertical-align: bottom; margin: 0; } input, textarea, select, .cbi-dropdown:not(.btn):not(.cbi-button), .uneditable-input { display: inline-block; width: 210px; padding: 4px; font-size: 13px; line-height: 18px; border: 1px solid #ccc; border-radius: 3px; } input, select, .cbi-dropdown:not(.btn):not(.cbi-button), .uneditable-input { height: 30px; } .uneditable-input { color: #808080; } .cbi-dropdown:not(.btn):not(.cbi-button), .cbi-dynlist { min-width: 210px; max-width: 400px; width: auto; } .cbi-dynlist { height: auto; min-height: 30px; display: inline-flex; flex-direction: column; } .cbi-dynlist > .item { margin-bottom: 4px; box-shadow: 0 0 2px #ccc; background: #fff; padding: 2px 2em 2px 4px; border: 1px solid #ccc; border-radius: 3px; position: relative; pointer-events: none; overflow: hidden; word-break: break-all; } .cbi-dynlist > .item::after { content: "×"; position: absolute; display: inline-flex; align-items: center; top: -1px; right: -1px; bottom: -1px; padding: 0 6px; border: 1px solid #ccc; border-radius: 0 3px 3px 0; font-weight: bold; color: #2e7d32; pointer-events: auto; } .cbi-dynlist > .add-item { display: flex; } .cbi-dynlist > .add-item > input, .cbi-dynlist > .add-item > button { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } select { padding: initial; background: #fff; box-shadow: inset 0 -1px 3px rgba(0, 0, 0, 0.1); } input[type=checkbox], input[type=radio] { width: auto; height: auto; padding: 0; margin: 3px 0; *margin-top: 0; /* IE6-7 */ line-height: normal; border: none; } input[type=file] { background-color: #fff; padding: initial; border: initial; line-height: initial; box-shadow: none; width: auto !important; } input[type=button], input[type=reset], input[type=submit] { width: auto; height: auto; } select[multiple] { height: inherit; background-color: #fff; } .td > input[type=text], .td > input[type=password], .td > select, .td > .cbi-dropdown:not(.btn):not(.cbi-button), .cbi-dynlist > .add-item > .cbi-dropdown { width: 100%; } .uneditable-input { background-color: #fff; display: block; border-color: #eee; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); cursor: not-allowed; } ::-moz-placeholder { color: #bfbfbf; } ::-webkit-input-placeholder { color: #bfbfbf; } .item::after, .btn, .cbi-button, input, button, textarea { transition: border linear 0.2s, box-shadow linear 0.2s; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); } /* ИЗМЕНЕНИЕ: Цвет фокуса на зеленый */ .item:hover::after, .btn:hover, .cbi-button:hover, button:hover, input:focus, textarea:focus { outline: 0; border-color: rgba(46, 125, 50, 0.8) !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(46, 125, 50, 0.6); text-decoration: none; } input[type=file]:focus, input[type=checkbox]:focus, select:focus { box-shadow: none; outline: 1px dotted #666; } input[disabled], button[disabled], select[disabled], textarea[disabled], input[readonly], button[readonly], select[readonly], textarea[readonly] { background-color: #f5f5f5; border-color: #ddd; pointer-events: none; cursor: default; } select[readonly], textarea[readonly] { pointer-events: auto; cursor: auto; } .cbi-optionals, .cbi-section-create { padding: 0 0 10px 10px; } .cbi-section-create { margin: -3px; display: inline-flex; align-items: center; } .cbi-section-create > * { margin: 3px; flex: 1 1 auto; } .cbi-section-create > * > input { width: 100%; } .actions, .cbi-page-actions { background: #f5f5f5; margin-bottom: 18px; padding: 17px 20px 18px 17px; border-top: 1px solid #ddd; border-radius: 0 0 3px 3px; text-align: right; } .actions .secondary-action, .cbi-page-actions .secondary-action{ float: right; } .actions .secondary-action a, .cbi-page-actions .secondary-action a { line-height: 30px; } .actions .secondary-action a:hover, .cbi-page-actions .secondary-action a:hover { text-decoration: underline; } .cbi-page-actions > form { display: inline; margin: 0; } .help-inline, .help-block { font-size: 13px; line-height: 18px; color: #bfbfbf; } .help-inline { padding-left: 5px; *position: relative; /* IE6-7 */ *top: -5px; /* IE6-7 */ } .help-block { display: block; max-width: 600px; } /* * Tables.less * Tables for, you guessed it, tabular data * ---------------------------------------- */ .tr { display: table-row; } .table[width="33%"], .th[width="33%"], .td[width="33%"] { width: 33%; } .table[width="100%"], .th[width="100%"], .td[width="100%"] { width: 100%; } .table { display: table; width: 100%; margin-bottom: 18px; padding: 0; font-size: 13px; border-collapse: collapse; position: relative; } .table .th, .table .td { display: table-cell; vertical-align: middle; /* Fixme */ padding: 10px 10px 9px; line-height: 18px; text-align: left; } .table .tr:first-child .th { padding-top: 9px; font-weight: bold; vertical-align: top; } .table .td, .table .th { border-top: 1px solid #ddd; } .tr.placeholder { height: calc(3em + 20px); } .tr.placeholder > .td { position: absolute; left: 0; right: 0; bottom: 0; text-align: center; line-height: 3em; } .tr.drag-over-above, .tr.drag-over-below { border: 2px solid #2e7d32; border-width: 2px 0 0 0; } .tr.drag-over-below { border-width: 0 0 2px 0; } /* Patterns.less * Repeatable UI elements outside the base styles provided from the scaffolding * ---------------------------------------------------------------------------- */ header { height: 40px; position: fixed; top: 0; left: 0; right: 0; z-index: 10000; overflow: visible; color: rgb(244,244,244); } /* ИЗМЕНЕНИЕ: Цвета хедера на зеленые */ header a { color: #2e7d32; } header h3 a:hover, header .brand:hover, header ul .active > a { color: #1b5e20; text-decoration: none; } header h3 { position: relative; } header h3 a, header .brand { float: left; display: block; padding: 8px 20px 12px; margin-left: 0px; color: #2e7d32; font-size: 20px; font-family: sans-serif; font-weight: 200; line-height: 1; outline:none; } .brand { display: inline-block; width: 135px !important; height: 40px !important; background-image: url('merona.svg'); background-size: contain; background-position: center; background-repeat: no-repeat; margin-right: 5px; } header p { margin: 0; line-height: 40px; } header .fill { background-color: #f2f2f2; background-repeat: repeat-x; -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); padding: 0 5px; } header div > ul, .nav { display: block; float: left; margin: 0 10px 0 0; position: relative; left: 0; } header div > ul > li, .nav > li { display: block; float: left; } header div > ul a, .nav a { display: block; float: none; padding: 10px 10px 11px; line-height: 19px; text-decoration: none; } header div > ul a:hover, .nav a:hover { color: #1b5e20; text-decoration: none; } header div > ul .active > a, .nav .active > a { background-color: #fff; background-color: rgba(0, 0, 0, 0.5); } header div > ul.secondary-nav, .nav.secondary-nav { float: right; margin-left: 10px; margin-right: 0; } header div > ul.secondary-nav .menu-dropdown, .nav.secondary-nav .menu-dropdown, header div > ul.secondary-nav .dropdown-menu, .nav.secondary-nav .dropdown-menu { right: 0; border: 0; } header div > ul a.menu:hover, .nav a.menu:hover, header div > ul li.open .menu, .nav li.open .menu, header div > ul .dropdown-toggle:hover, .nav .dropdown-toggle:hover, header div > ul .dropdown.open .dropdown-toggle, .nav .dropdown.open .dropdown-toggle { background: #444; background: rgba(255, 255, 255, 0.05); } header div > ul .menu-dropdown, .nav .menu-dropdown, header div > ul .dropdown-menu, .nav .dropdown-menu { background-color: #f2f2f2; } header div > ul .menu-dropdown a.menu, .nav .menu-dropdown a.menu, header div > ul .dropdown-menu a.menu, .nav .dropdown-menu a.menu, header div > ul .menu-dropdown .dropdown-toggle, .nav .menu-dropdown .dropdown-toggle, header div > ul .dropdown-menu .dropdown-toggle, .nav .dropdown-menu .dropdown-toggle { color: #f2f2f2; } header div > ul .menu-dropdown a.menu.open, .nav .menu-dropdown a.menu.open, header div > ul .dropdown-menu a.menu.open, .nav .dropdown-menu a.menu.open, header div > ul .menu-dropdown .dropdown-toggle.open, .nav .menu-dropdown .dropdown-toggle.open, header div > ul .dropdown-menu .dropdown-toggle.open, .nav .dropdown-menu .dropdown-toggle.open { background: #444; background: rgba(255, 255, 255, 0.05); } header div > ul .menu-dropdown li a, .nav .menu-dropdown li a, header div > ul .dropdown-menu li a, .nav .dropdown-menu li a { color: #666; text-shadow: none !important; } header div > ul .menu-dropdown li a:hover, .nav .menu-dropdown li a:hover, header div > ul .dropdown-menu li a:hover, .nav .dropdown-menu li a:hover { background-color: #ffffff; background-repeat: repeat-x; color: #2e7d32; text-shadow: none !important; } header div > ul .menu-dropdown .active a, .nav .menu-dropdown .active a, header div > ul .dropdown-menu .active a, .nav .dropdown-menu .active a { color: #ffffff; } header div > ul .menu-dropdown .divider, .nav .menu-dropdown .divider, header div > ul .dropdown-menu .divider, .nav .dropdown-menu .divider { background-color: #222; border-color: #444; } header ul .menu-dropdown li a, header ul .dropdown-menu li a { padding: 4px 15px; } li.menu, .dropdown { position: relative; } /* ИЗМЕНЕНИЕ: Цвет стрелки выпадающего меню на зеленый */ a.menu:after, .dropdown-toggle:after { width: 0; height: 0; display: inline-block; content: "↓"; text-indent: -99999px; vertical-align: top; margin-top: 8px; margin-left: 4px; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid #2e7d32; opacity: 0.5; } .menu-dropdown, .dropdown-menu { background-color: #ffffff; float: left; position: absolute; top: 40px; left: -9999px; z-index: 900; min-width: 160px; max-width: 220px; _width: 160px; margin-left: 0; margin-right: 0; padding: 6px 0; zoom: 1; border-color: #999; border-color: rgba(0, 0, 0, 0.2); border-style: solid; border-width: 0 1px 1px; border-radius: 0 0 6px 6px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); background-clip: padding-box; } .menu-dropdown li, .dropdown-menu li { float: none; display: block; background-color: none; } .menu-dropdown .divider, .dropdown-menu .divider { height: 1px; margin: 5px 0; overflow: hidden; background-color: #eee; border-bottom: 1px solid #ffffff; } header .dropdown-menu a, .dropdown-menu a { display: block; padding: 4px 15px; clear: both; font-weight: normal; line-height: 18px; color: #808080; text-shadow: 0 1px 0 #ffffff; } header .dropdown-menu a:hover, .dropdown-menu a:hover, header .dropdown-menu a.hover, .dropdown-menu a.hover { background-color: #dddddd; background-repeat: repeat-x; background-image: linear-gradient(to bottom, #eee, #ddd); color: #404040; text-decoration: none; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.025), inset 0 -1px rgba(0, 0, 0, 0.025); } .open .menu, .dropdown.open .menu, .open .dropdown-toggle, .dropdown.open .dropdown-toggle { color: #fff; background: #ccc; background: rgba(0, 0, 0, 0.3); } .open .menu-dropdown, .dropdown.open .menu-dropdown, .open .dropdown-menu, .dropdown.open .dropdown-menu { left: 0; } .dropdown:hover ul.dropdown-menu { left: 0; } .dropdown-menu .dropdown-menu { position: absolute; left: 159px; } .dropdown-menu li { position: relative; } .tabs, .cbi-tabmenu { margin: 0 -5px 18px; padding: 0 2px; list-style: none; display: flex; flex-wrap: wrap; background: linear-gradient(#fff 28px, #ddd 28px); background-size: 1px 29px; background-position: left bottom; } /* ИЗМЕНЕНИЕ: Цвет вкладок на зеленый */ .tabs > li, .cbi-tabmenu > li { flex: 0 1 auto; display: flex; align-items: center; height: 25px; max-width: 48%; margin: 4px 2px 0 2px; background: #fff; border: 1px solid #ddd; border-bottom: none; border-radius: 4px 4px 0 0; color: #2e7d32; } .tabs > li > a, .cbi-tabmenu > li > a { padding: 4px 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: inherit; text-decoration: none; border-radius: 4px 4px 0 0; line-height: 25px; outline: none; } .tabs > li:not(.active):hover, .cbi-tabmenu > .cbi-tab-disabled:hover { background: linear-gradient(#fff 90%, #ddd 100%); } .tabs > li:not(.active), .cbi-tabmenu > .cbi-tab-disabled { color: #999; background: linear-gradient(#eee 90%, #ddd 100%); } .cbi-tab-disabled[data-errors]::after { content: attr(data-errors); background: #c43c35; color: #fff; min-width: 12px; line-height: 14px; border-radius: 7px; text-align: center; margin: 0 5px 0 0; padding: 1px 2px; } .cbi-tabmenu.map { margin: 0; } .cbi-tabmenu.map > li { font-size: 16.5px; font-weight: bold; } .cbi-tabcontainer > fieldset.cbi-section[id] > legend { display: none; } .tabs .menu-dropdown, .tabs .dropdown-menu { top: 35px; border-width: 1px; border-radius: 0 6px 6px 6px; } .tabs a.menu:after, .tabs .dropdown-toggle:after { border-top-color: #999; margin-top: 15px; margin-left: 5px; } .tabs li.open.menu .menu, .tabs .open.dropdown .dropdown-toggle { border-color: #999; } .tabs li.open a.menu:after, .tabs .dropdown.open .dropdown-toggle:after { border-top-color: #555; } .tab-content > .tab-pane, .tab-content > div { display: none; } .tab-content > .active { display: block; } .breadcrumb { padding: 7px 14px; margin: 0 0 18px; background-color: #f5f5f5; background-repeat: repeat-x; background-image: linear-gradient(to bottom, #fff, #f5f5f5); border: 1px solid #ddd; border-radius: 3px; box-shadow: inset 0 1px 0 #fff; } .breadcrumb li { display: inline; text-shadow: 0 1px 0 #fff; } .breadcrumb .divider { padding: 0 5px; color: #bfbfbf; } .breadcrumb .active a { color: #404040; } footer { margin-top: 17px; padding-top: 17px; border-top: 1px solid #eee; } #modal_overlay { position: fixed; top: 0; bottom: 0; left: -10000px; right: 10000px; background: rgba(0, 0, 0, 0.7); z-index: 900; overflow-y: scroll; -webkit-overflow-scrolling: touch; transition: opacity .125s ease-in; opacity: 0; visibility: hidden; } .modal { width: 90%; margin: 5em auto; display: flex; flex-wrap: wrap; min-height: 32px; max-width: 600px; align-items: center; border-radius: 3px; background: #fff; box-shadow: 0 0 3px #444; padding: 1em 1em .5em 1em; min-width: 270px; } .modal > * { flex-basis: 100%; line-height: normal; margin-bottom: .5em; } .modal > pre, .modal > textarea { white-space: pre-wrap; overflow: auto; } body.modal-overlay-active { overflow: hidden; height: 100vh; } body.modal-overlay-active #modal_overlay { left: 0; right: 0; opacity: 1; visibility: visible; } .btn.danger, .alert-message.danger, .btn.danger:hover, .alert-message.danger:hover, .btn.error, .alert-message.error, .btn.error:hover, .alert-message.error:hover, .btn.success, .alert-message.success, .btn.success:hover, .alert-message.success:hover, .btn.info, .alert-message.info, .btn.info:hover, .alert-message.info:hover, .cbi-tooltip.error, .cbi-tooltip.success, .cbi-tooltip.info { color: #fff; } .btn .close, .alert-message .close { font-family: Arial, sans-serif; line-height: 18px; } .modal .btn.danger, .modal .btn { white-space: normal; } .btn.danger, .alert-message.danger, .btn.error, .alert-message.error, .cbi-tooltip.error { background: linear-gradient(to bottom, #c43c35, #882a25) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .btn.success, .alert-message.success, .cbi-tooltip.success { background: linear-gradient(to bottom, #62c462, #57a957) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .btn.info, .alert-message.info, .cbi-tooltip.info { background: linear-gradient(to bottom, #5bc0de, #339bb9) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .alert-message.notice, .cbi-tooltip.notice { background: linear-gradient(to bottom, #efefef, #fefefe) repeat-x; text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .item::after, .btn, .cbi-button { cursor: pointer; display: inline-block; background: #fff; padding: 5px 14px 6px; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); color: #333; font-size: 13px; line-height: normal; border: 1px solid #ccc; border-bottom-color: #bbb; border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); white-space: pre; } .btn:focus, .cbi-button:focus { outline: 1px dotted #666; } .cbi-input-invalid, .cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button), .cbi-input-invalid.cbi-dropdown:not([open]) > ul > li, .cbi-value-error input { color: #f00; border-color: #f00; } .cbi-button-neutral, .cbi-button-download, .cbi-button-find, .cbi-button-link, .cbi-button-up, .cbi-button-down { color: #444; } /* ИЗМЕНЕНИЕ: Основные кнопки на зеленый */ .btn.primary, .cbi-button-apply, .cbi-button-negative { border-color: #2e7d32; color: #2e7d32; text-shadow: none !important; } .cbi-section-remove .cbi-button, .cbi-button-reset { border-color: #2e7d32; color: #2e7d32; } .cbi-page-actions::after { display: table; content: ""; clear: both; } .cbi-page-actions > * { vertical-align: middle; } .cbi-page-actions > :not([method="post"]):not(.cbi-button-apply):not(.cbi-button-negative):not(.cbi-button-save):not(.cbi-button-reset) { float: left; margin-right: .4em; } /* ИЗМЕНЕНИЕ: Активные кнопки на зеленый */ .cbi-button-edit, .cbi-button-positive.important, .cbi-button-positive, .cbi-button-action.important, .cbi-button-action, .cbi-button-save, .cbi-button-reload, .cbi-page-actions .cbi-button-apply { color: #FFFFFF; background: #4caf50; border-color: #2e7d32; text-shadow: none !important; } .cbi-button-fieldadd, .cbi-section-remove .cbi-button, .cbi-button-remove, .cbi-button-add, .cbi-button-negative.important, .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-reset, .cbi-section-actions .cbi-button-action { color: #2e7d32; background: #ffffff; border-color: #2e7d32; text-shadow: none !important; } .cbi-page-actions .cbi-button-reset:hover, .cbi-page-actions .cbi-button-save:hover, .cbi-page-actions .cbi-button-apply:hover { color: #FFFFFF; background: #2e7d32; border-color: #2e7d32; text-shadow: none !important; } .cbi-dropdown { display: inline-flex !important; cursor: pointer; height: auto; position: relative; padding: 0 !important; } .cbi-dropdown:not(.btn):not(.cbi-button) { background: linear-gradient(#fff 0%, #e9e8e6 100%); border: 1px solid #ccc; border-radius: 3px; color: #404040; } .cbi-dynlist > .item:focus, .cbi-dropdown:focus { outline: 2px solid #4caf50; } .cbi-dropdown > ul { margin: 0 !important; padding: 0; list-style: none; overflow-x: hidden; overflow-y: auto; display: flex; width: 100%; } .cbi-dropdown.btn > ul:not(.dropdown), .cbi-dropdown.cbi-button > ul:not(.dropdown) { margin: 0 0 0 13px !important; } .cbi-dropdown.btn.spinning > ul:not(.dropdown), .cbi-dropdown.cbi-button.spinning > ul:not(.dropdown) { margin: 0 !important; } .cbi-dropdown > ul.preview { display: none; } .cbi-dropdown > .open, .cbi-dropdown > .more { flex-grow: 0; flex-shrink: 0; display: flex; flex-direction: column; justify-content: center; text-align: center; line-height: 2em; padding: 0 .25em; } .cbi-dropdown.btn > .open, .cbi-dropdown.cbi-button > .open { padding: 0 .5em; margin-left: .5em; border-left: 1px solid; } .cbi-dropdown > .more, .cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder] { color: #777; font-weight: bold; text-shadow: 1px 1px 0px #fff; display: none; justify-content: center; } .cbi-dropdown > ul > li { display: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; flex-grow: 1; align-items: center; align-self: center; color: inherit; } .cbi-dropdown > ul.dropdown > li, .cbi-dropdown:not(.btn):not(.cbi-button) > ul > li { min-height: 20px; padding: .25em; color: #404040; } .cbi-dropdown > ul > li .hide-open { display: block; display: initial; } .cbi-dropdown > ul > li .hide-close { display: none; } .cbi-dropdown > ul > li[display]:not([display="0"]) { border-left: 1px solid #ccc; } .cbi-dropdown[empty] > ul { max-width: 1px; } .cbi-dropdown > ul > li > form { display: none; margin: 0; padding: 0; pointer-events: none; } .cbi-dropdown > ul > li img { vertical-align: middle; margin-right: .25em; } .cbi-dropdown > ul > li > form > input[type="checkbox"] { margin: 0; } .cbi-dropdown > ul > li input[type="text"] { height: 20px; } .cbi-dropdown[open] { position: relative; } .cbi-dropdown[open] > ul.dropdown { display: block; background: #f6f6f5; border: 1px solid #918e8c; box-shadow: 0 0 4px #918e8c; position: absolute; z-index: 1100; max-width: none; min-width: 100%; width: auto; transition: max-height .125s ease-in; } .cbi-dropdown > ul > li[display], .cbi-dropdown[open] > ul.preview, .cbi-dropdown[open] > ul.dropdown > li, .cbi-dropdown[multiple] > ul > li > label, .cbi-dropdown[multiple][open] > ul.dropdown > li, .cbi-dropdown[multiple][more] > .more, .cbi-dropdown[multiple][empty] > .more { flex-grow: 1; display: flex !important; } .cbi-dropdown[empty] > ul > li, .cbi-dropdown[optional][open] > ul.dropdown > li[placeholder], .cbi-dropdown[multiple][open] > ul.dropdown > li > form { display: block !important; } .cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; } .cbi-dropdown[open] > ul.dropdown > li .hide-close { display: block; display: initial; } .cbi-dropdown[open] > ul.dropdown > li { border-bottom: 1px solid #ccc; } .cbi-dropdown[open] > ul.dropdown > li[selected] { background: #2e7d32; } .cbi-dropdown[open] > ul.dropdown > li.focus { background: #81c784; } .cbi-dropdown[open] > ul.dropdown > li:last-child { margin-bottom: 0; border-bottom: none; } .cbi-dropdown[open] > ul.dropdown > li[unselectable] { opacity: 0.7; } .cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { width: 100%; } .cbi-dropdown[disabled] { pointer-events: none; opacity: .6; } input[type="text"] + .cbi-button, input[type="password"] + .cbi-button, select + .cbi-button { border-radius: 0 3px 3px 0; border-color: #ccc; margin-left: -2px; padding: 0 6px; vertical-align: top; height: 30px; font-size: 14px; line-height: 28px; } input[type="text"] + .cbi-button-add { color: #fff; border-radius: 0 3px 3px 0; border-color: #2e7d32; background: #2e7d32; } input[type="text"] + .cbi-button-remove { color: #124; border-radius: 0 3px 3px 0; border-color: #ccc; } .cbi-title-ref { color: #2e7d32; } .cbi-title-ref::after { content: "➙"; } .cbi-tooltip-container { cursor: help; } .cbi-tooltip { position: absolute; z-index: 1000; left: -10000px; box-shadow: 0 0 2px #ccc; border-radius: 3px; background: #fff; white-space: pre; padding: 2px 5px; opacity: 0; transition: opacity .25s ease-in; } .cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { left: auto; opacity: 1; transition: opacity .25s ease-in; } .cbi-progressbar { border: 1px solid var(--border-color-high); border-radius: 3px; position: relative; min-width: 170px; height: 8px; margin: 1.4em 0 4px 0; background: var(--background-color-medium); } .cbi-progressbar > div { background: var(--primary-color-medium); height: 100%; transition: width .25s ease-in; width: 0%; border-radius: 2px; } .cbi-progressbar::before { position: absolute; top: -1.4em; left: 0; content: attr(title); white-space: pre; overflow: hidden; text-overflow: ellipsis; } .zonebadge .cbi-tooltip { padding: 1px; background: inherit; margin: -1.6em 0 0 -5px; border-radius: 3px; pointer-events: none; box-shadow: 0 0 3px #444; } .zonebadge .cbi-tooltip > * { margin: 1px; } .zone-forwards { display: flex; flex-wrap: wrap; } .zone-forwards > * { flex: 1 1 40%; padding: 1px; } .zone-forwards > span { flex-basis: 10%; text-align: center; } .zone-forwards .zone-src, .zone-forwards .zone-dest { display: flex; flex-direction: column; } .btn.active, .btn:active { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn.disabled { cursor: default; opacity: 0.65; box-shadow: none; } .btn[disabled] { cursor: default; opacity: 0.65; box-shadow: none; } .btn.large { font-size: 15px; line-height: normal; padding: 9px 14px 9px; border-radius: 6px; } .btn.small { padding: 7px 9px 7px; font-size: 11px; } button.btn::-moz-focus-inner, input[type=submit].btn::-moz-focus-inner { padding: 0; border: 0; } .close { float: right; color: #000; font-size: 20px; font-weight: bold; line-height: 13.5px; text-shadow: 0 1px 0 #fff; opacity: 0.25; } .close:hover { color: #000; text-decoration: none; opacity: 0.4; } .alert-message { position: relative; padding: .5em .5em .25em .5em; margin-bottom: .5em; color: #404040; background: linear-gradient(to bottom, #fceec1, #eedc94) repeat-x; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); border-width: 1px; border-style: solid; border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); } .alert-message .close { margin-top: 1px; *margin-top: 0; } .alert-message h4, .alert-message h5, .alert-message pre, .alert-message ul, .alert-message li, .alert-message p { color: inherit; border: none; line-height: inherit; background: transparent; padding: 0; margin: .25em 0; } .alert-message button { margin: .25em 0; } .label, header [data-indicator] { padding: 1px 3px 2px; font-size: 9.75px; font-weight: bold; color: #fff !important; text-transform: uppercase; white-space: nowrap; background-color: #2e7d32; border-radius: 3px; text-shadow: none; margin-left: .4em; } header [data-indicator][data-clickable] { cursor: pointer; } a.label:link, a.label:visited { color: #fff; } a.label:hover { text-decoration: none; } .label.important { background-color: #2e7d32; } .label.warning { background-color: #f89406; } .label.success { background-color: #4caf50; } .label.notice, header [data-indicator][data-style="active"] { background-color: #81c784; } /* LuCI specific items */ .hidden { display: none } form.inline { display: inline; margin-bottom: 0; } header .pull-right { padding-top: 8px; } #modemenu li:last-child span.divider { display: none } #syslog { width: 100%; } .cbi-section-table .tr:hover .td, .cbi-section-table .tr:hover .th, .cbi-section-table .tr:hover::before { background-color: #f5f5f5; } .cbi-section-table .tr.cbi-section-table-descr .th { font-weight: normal; } .cbi-section-table-titles.named::before, .cbi-section-table-descr.named::before, .cbi-section-table-row[data-title]::before { content: attr(data-title) " "; display: table-cell; padding: 10px 10px 9px; line-height: 18px; font-weight: bold; vertical-align: middle; } .cbi-section-table-titles.named::before, .cbi-section-table-descr.named::before, .cbi-section-table-row[data-title]::before { border-top: 1px solid #ddd; } .left { text-align: left !important; } .right { text-align: right !important; margin-bottom: 5px !important;} .center { text-align: center !mportant; } .top { vertical-align: top !important; } .middle { vertical-align: middle !important; } .bottom { vertical-align: bottom !important; } .cbi-value-field { line-height: 1.5em; } .cbi-value-field input[type=checkbox], .cbi-value-field input[type=radio] { margin-top: 8px; margin-right: 6px; } table table td, .cbi-value-field table td { border: none; } .table.cbi-section-table input[type="password"], .table.cbi-section-table input[type="text"], .table.cbi-section-table textarea, .table.cbi-sectable select { width: 100%; } .table.cbi-section-table .td.cbi-section-table-cell { white-space: nowrap; text-align: right; } .table.cbi-section-table .td.cbi-section-table-cell select { width: inherit; } .td.cbi-section-actions { text-align: right; vertical-align: middle; } .td.cbi-section-actions > * { display: flex; } .td.cbi-section-actions > * > *, .td.cbi-section-actions > * > form > * { flex: 1 1 4em; margin: 0 1px; } .td.cbi-section-actions > * > form { display: inline-flex; margin:0; } .table.valign-middle .td { vertical-align: middle; } .cbi-rowstyle-2, .tr.table-titles, .tr.cbi-section-table-titles { background: #f9f9f9; } .cbi-value-description { background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgdmlld0JveD0nMCAwIDE2IDE2Jz48Y2lyY2xlIGN4PSc4JyBjeT0nOCcgcj0nNy41JyBmaWxsPScjZThmNWU5JyBzdHJva2U9JyMzODhlM2MnIHN0cm9rZS13aWR0aD0nMS4yJy8+PHRleHQgeD0nOCcgeT0nMTInIGZvbnQtc2l6ZT0nMTEnIGZvbnQtZmFtaWx5PSdzYW5zLXNlcmlmJyBmb250LXdlaWdodD0nYm9sZCcgdGV4dC1hbmNob3I9J21pZGRsZScgZmlsbD0nIzJlN2QzMic+PzwvdGV4dD48L3N2Zz4="); background-position: .25em .2em; background-repeat: no-repeat; margin: .25em 0 0 0; padding: 0 0 0 1.7em; } .cbi-section-error { border: 1px solid #f00; border-radius: 3px; background-color: #c8e6c9; padding: 5px; margin-bottom: 18px; } .cbi-section-error ul { ma 0 0 0 20px; } .cbi-section-error ul li { color: #f00; font-weight: bold; } .ifacebox { background-color: #fff; border: 1px solid #ccc; margin: 0 10px; text-align: center; white-space: nowrap; background-image: linear-gradient(#fff, #fff 25%, #f9f9f9); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); display: inline-flex; flex-direction: column; line-height: 1.2em; min-width: 100px; } .ibox .ifacebox-head { border-bottom: 1px solid #ccc; padding: 2px; background: #eee; } .ifacebox .ifacebox-head.active { background: #4caf50; } .ifacebox .ifacebox-body { padding: .25em; } .ifacebadge { display: inline-block; flex-direction: row; white-space: nowrap; background-color: #fff; border: 1px solid #ccc; padding: 2px; background-image: linear-gradient(#fff, #fff 25%, #f9f9f9); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255,, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: default; line-height: 1.2em; } .ifacebadge img { width: 16px; height: 16px; vertical-align: middle; } .ifacebadge-active { border-color: #000; font-weight: bold; } .network-status-table { display: flex; flex-wrap: wrap; } .network-status-table .ifacebox { margin: .5em; flex-grow: 1; } .network-status-table .ifacebox-body { display: flex; flex-direction: column; height: 100%; text-align: left; } .network-status-table .ifacebox-body > * gin: .25em; } .network-status-table .ifacebox-body > span { flex: 10 10 auto; height: 100%; } .network-status-table .ifacebox-body > div { margin: -.125em; display: flex; flex-wrap: wrap; } #dsl_status_table .ifacebox-body span > strong { display: inline-block; min-width: 35%; } .ifacebadge.large, .network-status-table .ifacebox-body .ifacebadge { display: flex; flex: 1; padding: .25em; min-width: 220px; margin: .125em; } .ifacebadge.large { display: inline-flex; } .network-status-table .x-body .ifacebadge > span { overflow: hidden; text-overflow: ellipsis; } .ifacebadge > *, .ifacebadge.large > * { margin: 0 .125em; } .zonebadge { padding: 2px; border-radius: 4px; display: inline-block; white-space: nowrap; color: #666; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); } .zonebadge > em, .zonebadge > strong { margin: 0 2px; display: inline-block; } .zonebadge input { width: 6em; } .zonebadge > .ifacebadge { margin-left: 2px; } .zonebadge-empty { border: 1px dashed #aaa; color: #aaa; font-style: italic; font-size: smaller; } div.cbi-value var, .td.cbi-value-field var { font-style: italic; color: #2e7d32; } div.cbi-value var[data-tooltip], .td.cbi-value-field var[data-tooltip], div.cbi-value var.cbi-tooltip-container, .td.cbi-value-field var.cbi-tooltip-container { cursor: help; border-bottom: 1px dotted #2e7d32; } div.cbi-value var.cbi-tooltip-container, .td.cbi-value-field var.cbi-tooltip-container .cbi-tooltip { font-style: normal; white-space: normal; color:4; } #modal_overlay > .modal.uci-dialog, #modal_overlay > .modal.cbi-modal { max-width: 900px; } .uci-change-list { line-height: 170%; white-space: pre; } .uci-change-list del, .uci-change-list ins, .uci-change-list var, .uci-change-legend-label del, .uci-change-legend-label ins, .uci-change-legend-label var { text-decoration: none; font-family: monospace; font-style: normal; border: 1px solid #ccc; background: #eee; padding: 2px; display: block; line-height: 15px; margin-bottom: 1px; } .uci-change-list ins, .uci-change-legend-label ins { border-color: #0f0; background: #cfc; } .uci-change-list del, .uci-change-legend-label del { border-color: #f00; background: #fcc; } .uci-change-list var, .uci-change-legend-label var { border-color: #ccc; background: #eee; } .uci-change-list var ins, .uci-change-list var del { display: inline-block; border: none; width: 100%; padding: 0; } .uci-change-legend { padding: 5px; } .uci-change-legend-label { width: 150px; float: left; } .uci-change-legend-label > ins, .uci-change-legend-label > del, .uci-change-legend-label > var { float: left; margin-right: 4px; width: 16px; height: 16px; display: block; position: relative; } .uci-change-legend-label var ins, .uci-change-legend-label var del { border: none; position: absolute; top: 2px; left: 2px; right: 2px; bottom: 2px; } #modal_overlay { position: fixed; top: 0; bottom: 0; left: -10000px; right: 10000px; background: rgba(0, 0, 0, 0.7); z-index: 900; overflow-y: scroll; -webkit-overflow-scrolling: touch; transition: opacity .125s ease-in; opacity: 0; } #modal_overlay > .modal { width: 90%; margin: 5em auto; display: flex; flex-wrap: wrap; min-height: 32px; max-width: 600px; align-items: center; border-radius: 3px; background: #fff; box-shadow: 0 0 3px #444; padding: 1em 1em .5em 1em; min-width: 270px; } #modal_overlay .modal > * { flex-basis: 100%; line-height: normal; margin-bottom: .5em; } #modal_overlay .modal > pre, #modal_overlay .modal > textarea { white-space: pre-wrap; overflow: auto; } body.modal-overlay-active { overflow: hidden; height: 100vh; } body.modal-overlay-active #modal_overlay { left: 0; right: 0; opacity: 1; } html body.apply-overlay-active { height: calc(100vh - 63px); } #applyreboot-section { line-height: 300%; } [data-page="admin-network-dhcp"] [data-name="ip"] { width: 15%; } @keyframes flash { 0% { opacity: 1; } 50% { opacity: .5; } 100% { opacity: 1; } } .flash { animation: flash .35s; } .spinning { position: relative; padding-left: 32px !important; } .spinning::before { position: absolute; top: 0; left: 0; bottom: 0; width: 32px; content: " "; background: url(../resources/icons/loading.svg) no-repeat center; background-size: 16px; } [data-tab-title] { height: 0; opacity: 0; overflow: hidden; } [data-tab-active="true"] { opacity: 1; height: auto; overflow: visible; transition: opacity .25s ease-in; } .cbi-filebrowser { min-width: 210px; max-width: 100%; border: 1px solid #ccc; border-radius: 3px; display: flex; flex-direction: column; opacity: 0; height: 0; overflow: hidden; } .cbi-filebrowser.open { opacity: 1; height: auto; overflow: visible; transition: opacity .25s ease-in; } .cbi-filebrowser > * { max-width: 100%; overflow: hidden; text-overflow: ellipsis; padding: 0 0 .25em 0; margin: .25em .25em 0px .25em; white-space: nowrap; border-bottom: 1px solid #ccc; } .cbi-filebrowser .cbi-button-positive { margin-right: .25em; } .cbi-filebrowser > div { border-bottom: none; } .cbi-filebrowser > ul > li { display: flex; flex-direction: row; } .cbi-filebrowser > ul > li:hover { background: #f5f5f5; } .cbi-filebrowser > ul > li > div:first-child { flex: 10; overflow: hidden; text-overflow: ellipsis; } .cbi-filebrowser > ul > li > div:last-child { flex: 3; text-align: right; } .cbi-filebrowser > ul > li > div:last-child > button { padding: .125em .25em; margin: 1px 0 1px .25em; } .cbi-filebrowser .upload { display: flex; flex-direction: row; flex-wrap: wrap; margin: 0 -.125em .25em -.125em; padding: 0 0 .125em 0px; border-bottom: 1px solid #ccc; } .cbi-filebrowser .upload > * { margin: .125em; flex: 1; } .cbi-filebrowser .upload > .btn { flex-basis: 60px; } .cbi-filebrowser .upload > div { flex: 10; min-width: 150px; } .cbi-filebrowser .upload > div > input { width: 100%; } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } .fade-in { animation: fade-in .4s ease; } .fade-out { animation: fade-out .4s ease; } .assoclist .ifacebadge { display: flex; flex-direction: column; align-items: center; white-space: normal; text-align: center; } .assoclist .ifacebadge > img { margin: .2em; } .assoclist .td:nth-of-type(3), .assoclist .td:nth-of-type(5) { width: 25%; } .assoclist .td:nth-of-type(6) button { word-break: normal; } img[src*="signal-0"] { filter: sepia(1) saturate(6) hue-rotate(80deg) brightness(0.65) contrast(1.1); } ================================================ FILE: luci/themes/luci-theme-merona/htdocs/luci-static/merona/checkbox.css ================================================ /* Custom checkbox and radio styles for green theme */ input[type="checkbox"], input[type="radio"] { appearance: none; -webkit-appearance: none; width: 18px; height: 18px; border: 2px solid #ccc; background: #fff; position: relative; cursor: pointer; vertical-align: middle; } input[type="checkbox"] { border-radius: 3px; } input[type="checkbox"]:checked { background-color: #2e7d32; border-color: #2e7d32; } input[type="checkbox"]:checked::before { content: "✓"; position: absolute; color: white; font-size: 14px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%); } input[type="radio"] { border-radius: 50%; } input[type="radio"]:checked { background-color: #2e7d32; border-color: #2e7d32; } input[type="radio"]:checked::after { content: ""; position: absolute; width: 8px; height: 8px; background: white; border-radius: 50%; top: 50%; left: 50%; transform: translate(-50%, -50%); } input[type="checkbox"]:hover, input[type="radio"]:hover { border-color: #2e7d32; } input[type="checkbox"]:focus, input[type="radio"]:focus { outline: none; box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.2); } ================================================ FILE: luci/themes/luci-theme-merona/htdocs/luci-static/merona/mobile.css ================================================ header h3 a, header .brand { display:none !important; } @media screen and (max-device-width: 600px) { #maincontent.container { margin-top: 30px; } .tabs, .cbi-tabmenu { background: linear-gradient(#fff 20%, #ddd 100%); background-size: 1px 34px; margin-bottom: 10px; } .tabs > li, .cbi-tabmenu > li { height: 30px; } .tabs > li > a, .cbi-tabmenu > li > a { padding: 0 8px; line-height: 30px; } .table { display: flex; flex-direction: column; width: 100%; } .tr { display: flex; flex-direction: row; flex-wrap: wrap; align-items: flex-end; border-top: 1px solid #ddd; padding: 5px 0; margin: 0 -3px; } .table .th, .table .td, .table .tr::before { flex: 2 2 33%; align-self: flex-start; overflow: hidden; text-overflow: ellipsis; word-wrap: break-word; display: inline-block; border-top: none; padding: 3px; box-sizing: border-box; } .table .td.cbi-dropdown-open { overflow: visible; } .col-1 { flex: 1 1 30px !important; -webkit-flex: 1 1 30px !important; } .col-2 { flex: 2 2 60px !important; -webkit-flex: 2 2 60px !important; } .col-3 { flex: 3 3 90px !important; -webkit-flex: 3 3 90px !important; } .col-4 { flex: 4 4 120px !important; -webkit-flex: 4 4 120px !important; } .col-5 { flex: 5 5 150px !important; -webkit-flex: 5 5 150px !important; } .col-6 { flex: 6 6 180px !important; -webkit-flex: 6 6 180px !important; } .col-7 { flex: 7 7 210px !important; -webkit-flex: 7 7 210px !important; } .col-8 { flex: 8 8 240px !important; -webkit-flex: 8 8 240px !important; } .col-9 { flex: 9 9 270px !important; -webkit-flex: 9 9 270px !important; } .col-10 { flex: 10 10 300px !important; -webkit-flex: 10 10 300px !important; } .td select { word-wrap: normal; } .td[data-widget="button"], .td[data-widget="fvalue"] { flex: 1 1 17%; text-align: left; } .td.cbi-value-field { align-self: flex-start; } .td.cbi-value-field .cbi-button { width: 100%; } .table.cbi-section-table { border: none; background: none; margin: 0; } .tr.table-titles, .cbi-section-table-titles, .cbi-section-table-descr { display: none; } .cbi-section-table-row { display: flex; flex-direction: row; flex-wrap: wrap; margin: 0 0 .5em 0; } .cbi-section-table + .cbi-section-create { padding-top: 0; } .tr[data-title]::before { display: block; flex: 1 1 100%; background: #f5f5f5 !important; font-size: 16px; border-bottom: 1px solid #ddd; } .td[data-title]::before, .td[data-description]::after { display: block; } .td[data-title] ~ .td.cbi-section-actions { align-self: flex-start; } .td[data-title] ~ .td.cbi-section-actions::before { display: block; content: "\a0"; } .td.cbi-section-actions { overflow: initial; max-width: 100%; padding: 3px 2px; } .hide-sm, .hide-xs { display: none !important; } .td.cbi-value-field { flex-basis: 100%; } .td.cbi-value-field[data-widget="dvalue"] { flex-basis: 50%; } .td.cbi-value-field[data-widget="button"], .td.cbi-value-field[data-widget="fvalue"] { flex-basis: 25%; text-align: left; } .cbi-section-table .tr:hover .td, .cbi-section-table .tr:hover .th, .cbi-section-table .tr:hover::before { background-color: transparent; } .cbi-value { padding-bottom: .5em; border-bottom: 1px solid #ddd; margin-bottom: .5em; } .cbi-value label.cbi-value-title { float: none; font-weight: bold; } .cbi-value-field, .cbi-dropdown { width: 100%; margin: 0; } input, textarea, select, .cbi-dropdown > ul > li input[type="text"] { font-size: 16px !important; line-height: 28px; height: auto; } select, input[type="text"], input[type="password"] { width: 100%; height: 30px; } input.cbi-input-password { width: calc(100% - 25px); } [data-dynlist] { display: block; } [data-dynlist] > .add-item > input { width: calc(100% - 21px); } [data-dynlist] > .add-item > .cbi-button { margin-right: -1px; } input[type="text"] + .cbi-button, input[type="password"] + .cbi-button, select + .cbi-button { font-size: 14px !important; line-height: 28px; height: 30px; box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; } .cbi-value-field input[type="checkbox"], .cbi-value-field input[type="radio"] { margin: 0; } .btn, .cbi-button { font-size: 14px !important; padding: 4px 8px; } .actions, .cbi-page-actions { border-top: none; margin-top: -.5em; padding: 8px; } [data-page="admin-status-overview"] .cbi-section:nth-of-type(1) .td:first-child, [data-page="admin-status-overview"] .cbi-section:nth-of-type(2) .td:first-child { flex-grow: 1; } header .pull-right .label { white-space: normal; display: inline-block; text-align: center; line-height: 12px; margin: 1px 0; } header > .fill { padding: 1px; } header > .fill > .container { display: flex; flex-direction: row; } header .nav { flex: 3 3 80%; margin: 2px 5px 2px 0; display: flex; flex-wrap: wrap; justify-content: flex-start; } header .nav a { padding: 2px 6px; } header .pull-right { flex: 1 1 20%; display: flex; flex-direction: column; padding: 0; justify-content: space-around; } .menu-dropdown, .dropdown-menu { top: 23px; } body { padding-top: 30px; } .cbi-optionals, .cbi-section-create { padding: 0 0 14px 0; } #cbi-network-switch_vlan .th, #cbi-network-switch_vlan .td { flex-basis: 12%; } #cbi-network-switch_vlan .td.cbi-section-actions { flex-basis: 100%; } #cbi-network-switch_vlan .td.cbi-section-actions::before { display: none; } #cbi-network-switch_vlan .td.cbi-section-actions > * { width: auto; display: block; } #wifi_assoclist_table .td, [data-page="admin-status-processes"] .td { flex-basis: 50% !important; } [data-page="admin-status-processes"] .td[data-widget="button"] { flex-basis: 33% !important; } [data-page="admin-status-processes"] .td[data-name="PID"], [data-page="admin-status-processes"] .td[data-name="USER"] { flex-basis: 25% !important; } [data-page="admin-system-fstab"] .td[data-widget="button"]::before, [data-page="admin-system-startup"] .td[data-widget="button"]::before, [data-page="admin-status-processes"] .td[data-widget="button"]::before { display: none; } } @media screen and (max-device-width: 375px) { #maincontent.container { margin-top: 55px; } .cbi-page-actions { display: flex; flex-wrap: wrap; justify-content: space-between; margin: 0 -1px; padding: 0; } .cbi-page-actions .cbi-button:not(.cbi-dropdown) { flex: 1 1 calc(50% - 2px); margin: 1px !important; overflow: hidden; text-overflow: ellipsis; } .cbi-page-actions .cbi-button-negative, .cbi-page-actions .cbi-button-primary, .cbi-page-actions .cbi-button-apply { flex-basis: calc(100% - -2px); } .cbi-section-actions .cbi-button { overflow: hidden; text-overflow: ellipsis; } body[data-page="admin-network-wireless"] .td[data-name="_badge"] { max-width: 50px; align-self: center; } body[data-page="admin-network-wireless"] .td[data-name="_badge"] .ifacebadge { display: flex; align-items: center; flex-direction: column; } body[data-page="admin-network-wireless"] .td[data-name="_stat"] { flex-basis: 60%; } body[data-page="admin-network-network"] .td.cbi-section-actions::before, body[data-page="admin-network-wireless"] .td.cbi-section-actions::before { content: none !important; } } @media screen and (max-device-width: 200px) { #maincontent.container { margin-top: 230px; } } @media screen and (max-width: 375px) { .td .ifacebox { width: 100%; margin: 0 !important; flex-direction: row; } .td .ifacebox .ifacebox-head { min-width: 25%; justify-content: space-around; } .td .ifacebox .ifacebox-head, .td .ifacebox .ifacebox-body { display: flex; border-bottom: none; align-items: center; } .td .ifacebox .ifacebox-head > *, .ifacebox .ifacebox-body > * { margin: .125em; } } ================================================ FILE: luci/themes/luci-theme-merona/htdocs/luci-static/resources/menu-merona.js ================================================ 'use strict'; 'require baseclass'; 'require ui'; return baseclass.extend({ __init__: function() { ui.menu.load().then(L.bind(this.render, this)); }, render: function(tree) { var node = tree, url = ''; this.renderModeMenu(tree); if (L.env.dispatchpath.length >= 3) { for (var i = 0; i < 3 && node; i++) { node = node.children[L.env.dispatchpath[i]]; url = url + (url ? '/' : '') + L.env.dispatchpath[i]; } if (node) this.renderTabMenu(node, url); } document.addEventListener('poll-start', this.handleBodyMargin); document.addEventListener('poll-stop', this.handleBodyMargin); document.addEventListener('uci-new-changes', this.handleBodyMargin); document.addEventListener('uci-clear-changes', this.handleBodyMargin); window.addEventListener('resize', this.handleBodyMargin); this.handleBodyMargin(); }, renderTabMenu: function(tree, url, level) { var container = document.querySelector('#tabmenu'), ul = E('ul', { 'class': 'tabs' }), children = ui.menu.getChildren(tree), activeNode = null; for (var i = 0; i < children.length; i++) { var isActive = (L.env.dispatchpath[3 + (level || 0)] == children[i].name), activeClass = isActive ? ' active' : '', className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass); ul.appendChild(E('li', { 'class': className }, [ E('a', { 'href': L.url(url, children[i].name) }, [ _(children[i].title) ] )])); if (isActive) activeNode = children[i]; } if (ul.children.length == 0) return E([]); container.appendChild(ul); container.style.display = ''; if (activeNode) this.renderTabMenu(activeNode, url + '/' + activeNode.name, (level || 0) + 1); return ul; }, renderMainMenu: function(tree, url, level) { var ul = level ? E('ul', { 'class': 'dropdown-menu' }) : document.querySelector('#topmenu'), children = ui.menu.getChildren(tree); if (children.length == 0 || level > 1) return E([]); for (var i = 0; i < children.length; i++) { var submenu = this.renderMainMenu(children[i], url + '/' + children[i].name, (level || 0) + 1), subclass = (!level && submenu.firstElementChild) ? 'dropdown' : null, linkclass = (!level && submenu.firstElementChild) ? 'menu' : null, linkurl = submenu.firstElementChild ? '#' : L.url(url, children[i].name); var li = E('li', { 'class': subclass }, [ E('a', { 'class': linkclass, 'href': linkurl }, [ _(children[i].title) ]), submenu ]); ul.appendChild(li); } ul.style.display = ''; return ul; }, renderModeMenu: function(tree) { var ul = document.querySelector('#modemenu'), children = ui.menu.getChildren(tree); for (var i = 0; i < children.length; i++) { var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0); ul.appendChild(E('li', { 'class': isActive ? 'active' : null }, [ E('a', { 'href': L.url(children[i].name) }, [ _(children[i].title) ]), ' ', E('span', { 'class': 'divider' }, [ '|' ]) ])); if (isActive) this.renderMainMenu(children[i], children[i].name); } if (ul.children.length > 1) ul.style.display = ''; }, handleBodyMargin: function(ev) { var body = document.querySelector('body'), head = document.querySelector('header'); body.style.marginTop = head.offsetHeight + 'px'; } }); ================================================ FILE: luci/themes/luci-theme-merona/luasrc/view/themes/merona/footer.htm ================================================ <%# Copyright 2008 Steven Barth Copyright 2008 Jo-Philipp Wich Copyright 2012 David Menting Licensed to the public under the Apache License 2.0. -%> <% local ver = require "luci.version" %>
<%= ver.distversion %> © merona.ru <%= os.date("%Y") %> 
================================================ FILE: luci/themes/luci-theme-merona/luasrc/view/themes/merona/header.htm ================================================ <%# Copyright 2008 Steven Barth Copyright 2008-2016 Jo-Philipp Wich Copyright 2012 David Menting Licensed to the public under the Apache License 2.0. -%> <% local sys = require "luci.sys" local util = require "luci.util" local http = require "luci.http" local disp = require "luci.dispatcher" local boardinfo = util.ubus("system", "board") local node = disp.context.dispatched -- send as HTML5 http.prepare_content("text/html") -%> <%=striptags( (boardinfo.hostname or "?") .. ( (node and node.title) and ' - ' .. translate(node.title) or '')) %> - LuCI <% if node and node.css then %> <% end -%> <% if css then %> <% end -%> ">
<%- if luci.sys.process.info("uid") == 0 and luci.sys.user.getuser("root") and not luci.sys.user.getpasswd("root") then -%>

<%:No password set!%>

<%:There is no password set on this router. Please configure a root password to protect the web interface.%>

<% if disp.lookup("admin/system/admin") then %> <% end %>
<%- end -%> ================================================ FILE: luci/themes/luci-theme-merona/root/etc/uci-defaults/30_luci-theme-merona ================================================ #!/bin/sh if [ "$PKG_UPGRADE" != 1 ]; then uci get luci.themes.Merona >/dev/null 2>&1 || \ uci batch <<-EOF set luci.themes.Merona=/luci-static/merona set luci.main.mediaurlbase=/luci-static/merona commit luci EOF fi exit 0 ================================================ FILE: luci/themes/luci-theme-routerich/Makefile ================================================ # # Copyright (C) 2008-2014 The LuCI Team # # This is free software, licensed under the Apache License, Version 2.0 . # include $(TOPDIR)/rules.mk LUCI_TITLE:=Routerich Theme LUCI_DEPENDS:= +luci-lua-runtime PKG_LICENSE:=Apache-2.0 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/resources/menu-routerich.js ================================================ 'use strict'; 'require baseclass'; 'require ui'; return baseclass.extend({ __init__: function () { ui.menu.load().then(L.bind(this.render, this)); }, render: function (tree) { var node = tree, url = '', children = ui.menu.getChildren(tree); for (var i = 0; i < children.length; i++) { var isActive = L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0; if (isActive) this.renderMainMenu(children[i], children[i].name); } if (L.env.dispatchpath.length >= 3) { for (var i = 0; i < 3 && node; i++) { node = node.children[L.env.dispatchpath[i]]; url = url + (url ? '/' : '') + L.env.dispatchpath[i]; } if (node) this.renderTabMenu(node, url); } document .querySelector('a.showSide') .addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle')); document .querySelector('.darkMask') .addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle')); }, handleMenuExpand: function (ev) { var a = ev.target, slide = a.parentNode, slide_menu = a.nextElementSibling; var collapse = false; document .querySelectorAll('.main .main-left .nav > li >ul.active') .forEach(function (ul) { $(ul).stop(true).slideUp('fast', function () { ul.classList.remove('active'); ul.previousElementSibling.classList.remove('active'); }); if (!collapse && ul === slide_menu) { collapse = true; } }); if (!slide_menu) return; if (!collapse) { $(slide) .find('.slide-menu') .slideDown('fast', function () { slide_menu.classList.add('active'); a.classList.add('active'); }); a.blur(); } ev.preventDefault(); ev.stopPropagation(); }, renderMainMenu: function (tree, url, level) { var l = (level || 0) + 1, ul = E('ul', { class: level ? 'slide-menu' : 'nav' }), children = ui.menu.getChildren(tree); if (children.length == 0 || l > 2) return E([]); for (var i = 0; i < children.length; i++) { var isActive = L.env.dispatchpath[l] == children[i].name && L.env.dispatchpath[l - 1] == tree.name, submenu = this.renderMainMenu(children[i], url + '/' + children[i].name, l), hasChildren = submenu.children.length, slideClass = hasChildren ? 'slide' : null, menuClass = hasChildren ? 'menu' : 'food'; if (isActive) { ul.classList.add('active'); slideClass += ' active'; menuClass += ' active'; } ul.appendChild( E( 'li', { class: slideClass }, [ E( 'a', { href: L.url(url, children[i].name), click: l == 1 ? ui.createHandlerFn(this, 'handleMenuExpand') : null, class: menuClass, 'data-title': hasChildren ? children[i].title.replace(' ', '_') : children[i].title.replace(' ', '_'), }, [_(children[i].title)] ), submenu, ] ) ); } if (l == 1) { document.querySelector('#mainmenu').appendChild(ul); document.querySelector('#mainmenu').style.display = ''; } return ul; }, renderTabMenu: function (tree, url, level) { var container = document.querySelector('#tabmenu'), l = (level || 0) + 1, ul = E('ul', { class: 'tabs' }), children = ui.menu.getChildren(tree), activeNode = null; if (children.length == 0) return E([]); for (var i = 0; i < children.length; i++) { var isActive = L.env.dispatchpath[l + 2] == children[i].name, activeClass = isActive ? ' active' : '', className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass); ul.appendChild( E('li', { class: className }, [ E('a', { href: L.url(url, children[i].name) }, [_(children[i].title)]), ]) ); if (isActive) activeNode = children[i]; } container.appendChild(ul); container.style.display = ''; if (activeNode) container.appendChild(this.renderTabMenu(activeNode, url + '/' + activeNode.name, l)); return ul; }, handleSidebarToggle: function (ev) { var showside = document.querySelector('a.showSide'), sidebar = document.querySelector('#mainmenu'), darkmask = document.querySelector('.darkMask'), scrollbar = document.querySelector('.main-right'); if (showside.classList.contains('active')) { showside.classList.remove('active'); sidebar.classList.remove('active'); scrollbar.classList.remove('active'); darkmask.classList.remove('active'); } else { showside.classList.add('active'); sidebar.classList.add('active'); scrollbar.classList.add('active'); darkmask.classList.add('active'); } }, }); ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/routerich/background/README.md ================================================ Drop background here! accept jpg png gif mp4 webm ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/routerich/css/cascade.css ================================================ /*! Pure v2.0.3 Copyright 2013 Yahoo! Licensed under the BSD License. https://github.com/pure-css/pure/blob/master/LICENSE.md */ /*! normalize.css v | MIT License | git.io/normalize Copyright (c) Nicolas Gallagher and Jonathan Neal */ /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ html { line-height: 1.15; -webkit-text-size-adjust: 100%; } body { margin: 0; } main { display: block; } h1 { font-size: 2em; margin: 0.67em 0; } hr { -webkit-box-sizing: content-box; box-sizing: content-box; height: 0; overflow: visible; } pre { font-family: monospace, monospace; font-size: 1em; } a { background-color: transparent; } abbr[title] { border-bottom: none; text-decoration: underline; -webkit-text-decoration: underline dotted; text-decoration: underline dotted; } b, strong { font-weight: bolder; } code, kbd, samp { font-family: monospace, monospace; font-size: 1em; } small { font-size: 80%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } img { border-style: none; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } button, select { text-transform: none; } [type="button"], [type="reset"], [type="submit"], button { -webkit-appearance: button; } [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner, button::-moz-focus-inner { border-style: none; padding: 0; } [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring, button:-moz-focusring { outline: 1px dotted ButtonText; } fieldset { padding: 0.35em 0.75em 0.625em; } legend { -webkit-box-sizing: border-box; box-sizing: border-box; color: inherit; display: table; max-width: 100%; padding: 0; white-space: normal; } progress { vertical-align: baseline; } textarea { overflow: auto; } [type="checkbox"], [type="radio"] { -webkit-box-sizing: border-box; box-sizing: border-box; padding: 0; } [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } [type="search"] { -webkit-appearance: textfield; outline-offset: -2px; } [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } details { display: block; } summary { display: list-item; } template { display: none; } [hidden] { display: none; } html { font-family: sans-serif; } .hidden, [hidden] { display: none !important; } .pure-img { max-width: 100%; height: auto; } .pure-g { letter-spacing: -0.31em; text-rendering: optimizespeed; font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; -ms-flex-flow: row wrap; flex-flow: row wrap; -ms-flex-line-pack: start; align-content: flex-start; } @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { table .pure-g { display: block; } } .opera-only :-o-prefocus, .pure-g { word-spacing: -0.43em; } .pure-u { display: inline-block; letter-spacing: normal; word-spacing: normal; vertical-align: top; text-rendering: auto; } .pure-g [class*="pure-u"] { font-family: sans-serif; } .pure-u-1, .pure-u-1-1, .pure-u-1-12, .pure-u-1-2, .pure-u-1-24, .pure-u-1-3, .pure-u-1-4, .pure-u-1-5, .pure-u-1-6, .pure-u-1-8, .pure-u-10-24, .pure-u-11-12, .pure-u-11-24, .pure-u-12-24, .pure-u-13-24, .pure-u-14-24, .pure-u-15-24, .pure-u-16-24, .pure-u-17-24, .pure-u-18-24, .pure-u-19-24, .pure-u-2-24, .pure-u-2-3, .pure-u-2-5, .pure-u-20-24, .pure-u-21-24, .pure-u-22-24, .pure-u-23-24, .pure-u-24-24, .pure-u-3-24, .pure-u-3-4, .pure-u-3-5, .pure-u-3-8, .pure-u-4-24, .pure-u-4-5, .pure-u-5-12, .pure-u-5-24, .pure-u-5-5, .pure-u-5-6, .pure-u-5-8, .pure-u-6-24, .pure-u-7-12, .pure-u-7-24, .pure-u-7-8, .pure-u-8-24, .pure-u-9-24 { display: inline-block; letter-spacing: normal; word-spacing: normal; vertical-align: top; text-rendering: auto; } .pure-u-1-24 { width: 4.1667%; } .pure-u-1-12, .pure-u-2-24 { width: 8.3333%; } .pure-u-1-8, .pure-u-3-24 { width: 12.5%; } .pure-u-1-6, .pure-u-4-24 { width: 16.6667%; } .pure-u-1-5 { width: 20%; } .pure-u-5-24 { width: 20.8333%; } .pure-u-1-4, .pure-u-6-24 { width: 25%; } .pure-u-7-24 { width: 29.1667%; } .pure-u-1-3, .pure-u-8-24 { width: 33.3333%; } .pure-u-3-8, .pure-u-9-24 { width: 37.5%; } .pure-u-2-5 { width: 40%; } .pure-u-10-24, .pure-u-5-12 { width: 41.6667%; } .pure-u-11-24 { width: 45.8333%; } .pure-u-1-2, .pure-u-12-24 { width: 50%; } .pure-u-13-24 { width: 54.1667%; } .pure-u-14-24, .pure-u-7-12 { width: 58.3333%; } .pure-u-3-5 { width: 60%; } .pure-u-15-24, .pure-u-5-8 { width: 62.5%; } .pure-u-16-24, .pure-u-2-3 { width: 66.6667%; } .pure-u-17-24 { width: 70.8333%; } .pure-u-18-24, .pure-u-3-4 { width: 75%; } .pure-u-19-24 { width: 79.1667%; } .pure-u-4-5 { width: 80%; } .pure-u-20-24, .pure-u-5-6 { width: 83.3333%; } .pure-u-21-24, .pure-u-7-8 { width: 87.5%; } .pure-u-11-12, .pure-u-22-24 { width: 91.6667%; } .pure-u-23-24 { width: 95.8333%; } .pure-u-1, .pure-u-1-1, .pure-u-24-24, .pure-u-5-5 { width: 100%; } .pure-button { display: inline-block; line-height: normal; white-space: nowrap; vertical-align: middle; text-align: center; cursor: pointer; -webkit-user-drag: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-box-sizing: border-box; box-sizing: border-box; } .pure-button::-moz-focus-inner { padding: 0; border: 0; } .pure-button-group { letter-spacing: -0.31em; text-rendering: optimizespeed; } .opera-only :-o-prefocus, .pure-button-group { word-spacing: -0.43em; } .pure-button-group .pure-button { letter-spacing: normal; word-spacing: normal; vertical-align: top; text-rendering: auto; } .pure-button { font-family: inherit; font-size: 100%; padding: 0.5em 1em; color: rgba(0, 0, 0, 0.8); border: none transparent; background-color: #e6e6e6; text-decoration: none; border-radius: 2px; } .pure-button-hover, .pure-button:focus, .pure-button:hover { background-image: -webkit-gradient( linear, left top, left bottom, from(transparent), color-stop(40%, rgba(0, 0, 0, 0.05)), to(rgba(0, 0, 0, 0.1)) ); background-image: linear-gradient( transparent, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1) ); } .pure-button:focus { outline: 0; } .pure-button-active, .pure-button:active { -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset, 0 0 6px rgba(0, 0, 0, 0.2) inset; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset, 0 0 6px rgba(0, 0, 0, 0.2) inset; border-color: #000; } .pure-button-disabled, .pure-button-disabled:active, .pure-button-disabled:focus, .pure-button-disabled:hover, .pure-button[disabled] { border: none; background-image: none; opacity: 0.4; cursor: not-allowed; -webkit-box-shadow: none; box-shadow: none; pointer-events: none; } .pure-button-hidden { display: none; } .pure-button-primary, .pure-button-selected, a.pure-button-primary, a.pure-button-selected { background-color: #0078e7; color: #fff; } .pure-button-group .pure-button { margin: 0; border-radius: 0; border-right: 1px solid rgba(0, 0, 0, 0.2); } .pure-button-group .pure-button:first-child { border-top-left-radius: 2px; border-bottom-left-radius: 2px; } .pure-button-group .pure-button:last-child { border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-right: none; } .pure-form input[type="color"], .pure-form input[type="date"], .pure-form input[type="datetime-local"], .pure-form input[type="datetime"], .pure-form input[type="email"], .pure-form input[type="month"], .pure-form input[type="number"], .pure-form input[type="password"], .pure-form input[type="search"], .pure-form input[type="tel"], .pure-form input[type="text"], .pure-form input[type="time"], .pure-form input[type="url"], .pure-form input[type="week"], .pure-form select, .pure-form textarea { padding: 0.5em 0.6em; display: inline-block; border: 1px solid #ccc; -webkit-box-shadow: inset 0 1px 3px #ddd; box-shadow: inset 0 1px 3px #ddd; border-radius: 4px; vertical-align: middle; -webkit-box-sizing: border-box; box-sizing: border-box; } .pure-form input:not([type]) { padding: 0.5em 0.6em; display: inline-block; border: 1px solid #ccc; -webkit-box-shadow: inset 0 1px 3px #ddd; box-shadow: inset 0 1px 3px #ddd; border-radius: 4px; -webkit-box-sizing: border-box; box-sizing: border-box; } .pure-form input[type="color"] { padding: 0.2em 0.5em; } .pure-form input[type="color"]:focus, .pure-form input[type="date"]:focus, .pure-form input[type="datetime-local"]:focus, .pure-form input[type="datetime"]:focus, .pure-form input[type="email"]:focus, .pure-form input[type="month"]:focus, .pure-form input[type="number"]:focus, .pure-form input[type="password"]:focus, .pure-form input[type="search"]:focus, .pure-form input[type="tel"]:focus, .pure-form input[type="text"]:focus, .pure-form input[type="time"]:focus, .pure-form input[type="url"]:focus, .pure-form input[type="week"]:focus, .pure-form select:focus, .pure-form textarea:focus { outline: 0; border-color: #129fea; } .pure-form input:not([type]):focus { outline: 0; border-color: #129fea; } .pure-form input[type="checkbox"]:focus, .pure-form input[type="file"]:focus, .pure-form input[type="radio"]:focus { outline: thin solid #129fea; outline: 1px auto #129fea; } .pure-form .pure-checkbox, .pure-form .pure-radio { margin: 0.5em 0; display: block; } .pure-form input[type="color"][disabled], .pure-form input[type="date"][disabled], .pure-form input[type="datetime-local"][disabled], .pure-form input[type="datetime"][disabled], .pure-form input[type="email"][disabled], .pure-form input[type="month"][disabled], .pure-form input[type="number"][disabled], .pure-form input[type="password"][disabled], .pure-form input[type="search"][disabled], .pure-form input[type="tel"][disabled], .pure-form input[type="text"][disabled], .pure-form input[type="time"][disabled], .pure-form input[type="url"][disabled], .pure-form input[type="week"][disabled], .pure-form select[disabled], .pure-form textarea[disabled] { cursor: not-allowed; background-color: #eaeded; color: #cad2d3; } .pure-form input:not([type])[disabled] { cursor: not-allowed; background-color: #eaeded; color: #cad2d3; } .pure-form input[readonly], .pure-form select[readonly], .pure-form textarea[readonly] { background-color: #eee; color: #777; border-color: #ccc; } .pure-form input:focus:invalid, .pure-form select:focus:invalid, .pure-form textarea:focus:invalid { color: #b94a48; border-color: #e9322d; } .pure-form input[type="checkbox"]:focus:invalid:focus, .pure-form input[type="file"]:focus:invalid:focus, .pure-form input[type="radio"]:focus:invalid:focus { outline-color: #e9322d; } .pure-form select { height: 2.25em; border: 1px solid #ccc; background-color: #fff; } .pure-form select[multiple] { height: auto; } .pure-form label { margin: 0.5em 0 0.2em; } .pure-form fieldset { margin: 0; padding: 0.35em 0 0.75em; border: 0; } .pure-form legend { display: block; width: 100%; padding: 0.3em 0; margin-bottom: 0.3em; color: #333; border-bottom: 1px solid #e5e5e5; } .pure-form-stacked input[type="color"], .pure-form-stacked input[type="date"], .pure-form-stacked input[type="datetime-local"], .pure-form-stacked input[type="datetime"], .pure-form-stacked input[type="email"], .pure-form-stacked input[type="file"], .pure-form-stacked input[type="month"], .pure-form-stacked input[type="number"], .pure-form-stacked input[type="password"], .pure-form-stacked input[type="search"], .pure-form-stacked input[type="tel"], .pure-form-stacked input[type="text"], .pure-form-stacked input[type="time"], .pure-form-stacked input[type="url"], .pure-form-stacked input[type="week"], .pure-form-stacked label, .pure-form-stacked select, .pure-form-stacked textarea { display: block; margin: 0.25em 0; } .pure-form-stacked input:not([type]) { display: block; margin: 0.25em 0; } .pure-form-aligned input, .pure-form-aligned select, .pure-form-aligned textarea, .pure-form-message-inline { display: inline-block; vertical-align: middle; } .pure-form-aligned textarea { vertical-align: top; } .pure-form-aligned .pure-control-group { margin-bottom: 0.5em; } .pure-form-aligned .pure-control-group label { text-align: right; display: inline-block; vertical-align: middle; width: 10em; margin: 0 1em 0 0; } .pure-form-aligned .pure-controls { margin: 1.5em 0 0 11em; } .pure-form .pure-input-rounded, .pure-form input.pure-input-rounded { border-radius: 2em; padding: 0.5em 1em; } .pure-form .pure-group fieldset { margin-bottom: 10px; } .pure-form .pure-group input, .pure-form .pure-group textarea { display: block; padding: 10px; margin: 0 0 -1px; border-radius: 0; position: relative; top: -1px; } .pure-form .pure-group input:focus, .pure-form .pure-group textarea:focus { z-index: 3; } .pure-form .pure-group input:first-child, .pure-form .pure-group textarea:first-child { top: 1px; border-radius: 4px 4px 0 0; margin: 0; } .pure-form .pure-group input:first-child:last-child, .pure-form .pure-group textarea:first-child:last-child { top: 1px; border-radius: 4px; margin: 0; } .pure-form .pure-group input:last-child, .pure-form .pure-group textarea:last-child { top: -2px; border-radius: 0 0 4px 4px; margin: 0; } .pure-form .pure-group button { margin: 0.35em 0; } .pure-form .pure-input-1 { width: 100%; } .pure-form .pure-input-3-4 { width: 75%; } .pure-form .pure-input-2-3 { width: 66%; } .pure-form .pure-input-1-2 { width: 50%; } .pure-form .pure-input-1-3 { width: 33%; } .pure-form .pure-input-1-4 { width: 25%; } .pure-form-message-inline { display: inline-block; padding-left: 0.3em; color: #666; vertical-align: middle; font-size: 0.875em; } .pure-form-message { display: block; color: #666; font-size: 0.875em; } @media only screen and (max-width: 480px) { .pure-form button[type="submit"] { margin: 0.7em 0 0; } .pure-form input:not([type]), .pure-form input[type="color"], .pure-form input[type="date"], .pure-form input[type="datetime-local"], .pure-form input[type="datetime"], .pure-form input[type="email"], .pure-form input[type="month"], .pure-form input[type="number"], .pure-form input[type="password"], .pure-form input[type="search"], .pure-form input[type="tel"], .pure-form input[type="text"], .pure-form input[type="time"], .pure-form input[type="url"], .pure-form input[type="week"], .pure-form label { margin-bottom: 0.3em; display: block; } .pure-group input:not([type]), .pure-group input[type="color"], .pure-group input[type="date"], .pure-group input[type="datetime-local"], .pure-group input[type="datetime"], .pure-group input[type="email"], .pure-group input[type="month"], .pure-group input[type="number"], .pure-group input[type="password"], .pure-group input[type="search"], .pure-group input[type="tel"], .pure-group input[type="text"], .pure-group input[type="time"], .pure-group input[type="url"], .pure-group input[type="week"] { margin-bottom: 0; } .pure-form-aligned .pure-control-group label { margin-bottom: 0.3em; text-align: left; display: block; width: 100%; } .pure-form-aligned .pure-controls { margin: 1.5em 0 0 0; } .pure-form-message, .pure-form-message-inline { display: block; font-size: 0.75em; padding: 0.2em 0 0.8em; } } .pure-menu { -webkit-box-sizing: border-box; box-sizing: border-box; } .pure-menu-fixed { position: fixed; left: 0; top: 0; z-index: 3; } .pure-menu-item, .pure-menu-list { position: relative; } .pure-menu-list { list-style: none; margin: 0; padding: 0; } .pure-menu-item { padding: 0; margin: 0; height: 100%; } .pure-menu-heading, .pure-menu-link { display: block; text-decoration: none; white-space: nowrap; } .pure-menu-horizontal { width: 100%; white-space: nowrap; } .pure-menu-horizontal .pure-menu-list { display: inline-block; } .pure-menu-horizontal .pure-menu-heading, .pure-menu-horizontal .pure-menu-item, .pure-menu-horizontal .pure-menu-separator { display: inline-block; vertical-align: middle; } .pure-menu-item .pure-menu-item { display: block; } .pure-menu-children { display: none; position: absolute; left: 100%; top: 0; margin: 0; padding: 0; z-index: 3; } .pure-menu-horizontal .pure-menu-children { left: 0; top: auto; width: inherit; } .pure-menu-active > .pure-menu-children, .pure-menu-allow-hover:hover > .pure-menu-children { display: block; position: absolute; } .pure-menu-has-children > .pure-menu-link:after { padding-left: 0.5em; content: "\25B8"; font-size: small; } .pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after { content: "\25BE"; } .pure-menu-scrollable { overflow-y: scroll; overflow-x: hidden; } .pure-menu-scrollable .pure-menu-list { display: block; } .pure-menu-horizontal.pure-menu-scrollable .pure-menu-list { display: inline-block; } .pure-menu-horizontal.pure-menu-scrollable { white-space: nowrap; overflow-y: hidden; overflow-x: auto; padding: 0.5em 0; } .pure-menu-horizontal .pure-menu-children .pure-menu-separator, .pure-menu-separator { background-color: #ccc; height: 1px; margin: 0.3em 0; } .pure-menu-horizontal .pure-menu-separator { width: 1px; height: 1.3em; margin: 0 0.3em; } .pure-menu-horizontal .pure-menu-children .pure-menu-separator { display: block; width: auto; } .pure-menu-heading { text-transform: uppercase; color: #565d64; } .pure-menu-link { color: #777; } .pure-menu-children { background-color: #fff; } .pure-menu-disabled, .pure-menu-heading, .pure-menu-link { padding: 0.5em 1em; } .pure-menu-disabled { opacity: 0.5; } .pure-menu-disabled .pure-menu-link:hover { background-color: transparent; } .pure-menu-active > .pure-menu-link, .pure-menu-link:focus, .pure-menu-link:hover { background-color: #eee; } .pure-menu-selected > .pure-menu-link, .pure-menu-selected > .pure-menu-link:visited { color: #000; } .pure-table { border-collapse: collapse; border-spacing: 0; empty-cells: show; border: 1px solid #cbcbcb; } .pure-table caption { color: #000; font: italic 85%/1 arial, sans-serif; padding: 1em 0; text-align: center; } .pure-table td, .pure-table th { border-left: 1px solid #cbcbcb; border-width: 0 0 0 1px; font-size: inherit; margin: 0; overflow: visible; padding: 0.5em 1em; } .pure-table thead { background-color: #e0e0e0; color: #000; text-align: left; vertical-align: bottom; } .pure-table td { background-color: transparent; } .pure-table-odd td { background-color: #f2f2f2; } .pure-table-striped tr:nth-child(2n-1) td { background-color: #f2f2f2; } .pure-table-bordered td { border-bottom: 1px solid #cbcbcb; } .pure-table-bordered tbody > tr:last-child > td { border-bottom-width: 0; } .pure-table-horizontal td, .pure-table-horizontal th { border-width: 0 0 1px 0; border-bottom: 1px solid #cbcbcb; } .pure-table-horizontal tbody > tr:last-child > td { border-bottom-width: 0; } @font-face { font-family: "Google Sans"; src: url("data:application/font-woff2;charset=utf-8;base64,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") format("woff2"), url("data:application/font-woff;charset=utf-8;base64,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") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "TypoGraphica"; src: url("../fonts/TypoGraphica.eot?#iefix") format("embedded-opentype"), url("../fonts/TypoGraphica.woff") format("woff"), url("../fonts/TypoGraphica.ttf") format("truetype"), url("../fonts/TypoGraphica.svg#TypoGraphica") format("svg"); font-weight: normal; font-style: normal; } @font-face { font-family: "argon"; src: url("../fonts/argon.eot?u6kthm"); src: url("../fonts/argon.eot?u6kthm#iefix") format("embedded-opentype"), url("../fonts/argon.ttf?u6kthm") format("truetype"), url("../fonts/argon.woff?u6kthm") format("woff"), url("../fonts/argon.svg?u6kthm#argon") format("svg"); font-weight: normal; font-style: normal; font-display: block; } [class^="icon-"], [class*=" icon-"] { font-family: "argon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-expand_more:before { content: "\e20b"; } .icon-menu:before { content: "\e20e"; } .icon-favorite:before { content: "\e291"; } .icon-spinner:before { content: "\e603"; } .icon-delete:before { content: "\e900"; } .icon-edit:before { content: "\e901"; } .icon-use:before { content: "\e902"; } .icon-loading:before { content: "\e903"; } .icon-switch:before { content: "\e904"; } .icon-error:before { content: "\e905"; } .icon-dashboard:before { content: "\e906"; } .icon-logout:before { content: "\e907"; } .icon-Network:before { content: "\e908"; } .icon-services:before { content: "\e909"; } .icon-system:before { content: "\e90a"; } .icon-vpn:before { content: "\e90b"; } .icon-storage:before { content: "\e90c"; } .icon-statistics:before { content: "\e90d"; } .icon-hello-world:before { content: "\e90e"; } .icon-angle-right:before { content: "\e90f"; } .icon-password:before { content: "\e910"; } .icon-user:before { content: "\e971"; } .icon-question:before { content: "\f059"; } .icon-docker:before { content: "\e911"; } .icon-control:before { content: "\e912"; } .icon-statistics1:before { content: "\e913"; } .icon-asterisk:before { content: "\e914"; } .icon-app:before { content: "\e915"; } :root { --primary: #5e72e4; --dark-primary: #483d8b; --main-color: #09c; --header-bg: #09c; --header-color: #fff; --bar-bg: #5e72e4; --menu-bg-color: #fff; --menu-color: #5f6368; --menu-color-hover: #202124; --main-menu-color: #202124; --submenu-bg-hover: #d4d4d4; --submenu-bg-hover-active: #09c; --blue: #5e72e4; --indigo: #5603ad; --purple: #8965e0; --pink: #f3a4b5; --red: #f5365c; --orange: #fb6340; --yellow: #ffd600; --green: #2dce89; --teal: #11cdef; --cyan: #2bffc6; --gray: #8898aa; --gray-dark: #32325d; --light: #ced4da; --lighter: #e9ecef; --secondary: #f7fafc; --success: #2dce89; --info: #11cdef; --warning: #fb6340; --danger: #f5365c; --light: #adb5bd; --dark: #212529; --default: #172b4d; --white: #fff; --neutral: #fff; --darker: black; --background-color: #f4f5f7; --login-form-bg-color: rgba(244, 245, 247, 0.8); --breakpoint-xs: 0; --breakpoint-sm: 576px; --breakpoint-md: 768px; --breakpoint-lg: 992px; --breakpoint-xl: 1200px; --blur-radius: 10px; --blur-opacity: 0.5; --blur-radius-dark: 10px; --blur-opacity-dark: 0.5; --font-family-sans-serif: "Google Sans", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --font-family-normal: Open Sans, PingFangSC-Regular, Microsoft Yahei, WenQuanYi Micro Hei, "Helvetica Neue", Helvetica, Hiragino Sans GB, sans-serif; } * { margin: 0px; padding: 0px; box-sizing: border-box; } html, body { margin: 0px; padding: 0px; height: 100%; font-size: 16px; font-family: "Google Sans", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; font-family: var(--font-family-sans-serif); } html { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } body { font-size: 0.875rem; background-color: #f4f5f7; background-color: var(--background-color); color: #32325d; color: var(--gray-dark); -webkit-tap-highlight-color: transparent; } textarea { padding: 0.2rem; } textarea:focus-visible { outline: none; border: 1px solid var(--primary); } ::selection { background-color: #5e72e4; background-color: var(--primary); color: #ffffff; color: var(--white); } ::placeholder { color: var(--lighter); } a:link, a:visited, a:active { color: #5e72e4; color: var(--primary); text-decoration: none; } a:hover { text-decoration: underline; } li { list-style-type: none; } .table { position: relative; display: table; } .tr { display: table-row; } .thead { display: table-header-group; } .tbody { display: table-row-group; } .tfoot { display: table-footer-group; } .td, .th { line-height: normal; display: table-cell; padding: 0.5em; text-align: center; vertical-align: middle; } .th { font-weight: bold; white-space: nowrap; } .tr.placeholder { height: 4em; } .tr.placeholder > .td { line-height: 3; position: absolute; right: 0; bottom: 0; left: 0; padding: 0.4rem 0 !important; text-align: center !important; background: inherit; } .td[width="33%"] { padding: 1.1em 1.5rem; } .table[width="33%"], .th[width="33%"], .td[width="33%"] { width: 33%; } .table[width="100%"], .th[width="100%"], .td[width="100%"] { width: 100%; } .col-1 { flex: 1 1 30px !important; } .col-2 { flex: 2 2 60px !important; } .col-3 { flex: 3 3 90px !important; } .col-4 { flex: 4 4 120px !important; } .col-5 { flex: 5 5 150px !important; } .col-6 { flex: 6 6 180px !important; } .col-7 { flex: 7 7 210px !important; } .col-8 { flex: 8 8 240px !important; } .col-9 { flex: 9 9 270px !important; } .col-10 { flex: 10 10 300px !important; } * { box-sizing: border-box; margin: 0; padding: 0; } .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { font-family: inherit; font-weight: normal; line-height: 1.1 !important; color: inherit; } select { padding: 0.36rem 0.8rem; color: #555; border: thin solid #ccc; background-color: #fff; background-image: none; } .btn, button, select, input, .cbi-dropdown { line-height: 1.5em; padding: 0.5rem 0.75rem; color: #8898aa; border: 1px solid #dee2e6; border-radius: 0.25rem; outline: 0; background-image: none; box-shadow: none; transition: box-shadow 0.15s ease; } select, .cbi-dropdown { width: inherit; cursor: default; } select:not([multiple="multiple"]):focus, input:not(.cbi-button):focus, .cbi-dropdown:focus { border-color: #5e72e4; border-color: var(--primary); box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); } .cbi-dropdown, select[multiple="multiple"] { height: auto; } pre { overflow: auto; } code { font-size: 0.875rem; padding: 1px 3px; color: #101010; border-radius: 2px; background: #ddd; } abbr { cursor: help; text-decoration: underline; color: #5e72e4; color: var(--primary); } hr { margin: 1rem 0; opacity: 0.1; border-color: #eee; } ul { line-height: normal; } li { list-style-type: none; } h1 { font-size: 2rem; padding-bottom: 10px; border-bottom: thin solid #eee; } h2 { margin: 0 0 1rem 0; font-size: 1.25rem; letter-spacing: 0.1rem; padding: 1rem 1.25rem; color: #32325d; border-radius: 0.25rem; background: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); font-weight: bold; } h3 { font-size: 1.1rem; line-height: 1; display: block; width: 100%; margin: 0; margin-bottom: 0; padding: 0.8755rem 1.25rem; color: #32325d; color: var(--gray-dark); border-radius: 0.25rem; background: #fff; font-weight: bold; } h4 { margin: 0; padding: 0.75rem 1.25rem; font-size: 0.875rem; font-weight: 600; color: #525f7f; font-weight: bold; } h4 em { padding: 0 0.5rem; } h5 { font-size: 1rem; margin: 2rem 0 0 0; padding-bottom: 10px; } .pull-right { float: right; } .pull-left { float: left; } .nowrap:not(.td) { white-space: nowrap; } [disabled="disabled"] { pointer-events: none; } .login-page { height: 100%; } .login-page .video { position: absolute; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background-color: #000; background-color: var(--darker); overflow: hidden; } .login-page .video video { width: 100%; height: auto; } .login-page .volume-control { position: fixed; right: 1rem; top: 1rem; width: 1.5rem; height: 1.5rem; z-index: 5000; cursor: pointer; background-size: contain; background-image: url(../img/volume_high.svg); } .login-page .volume-control.mute { background-image: url(../img/volume_off.svg); } .login-page .main-bg { position: absolute; width: 100%; height: 100%; left: 0; top: 0; background-image: url(../img/blank.png); background-repeat: no-repeat; background-position: center; background-size: cover; transition: all 0.5s; } .login-page .login-container { height: 100%; margin-left: 4.5rem; position: absolute; top: 0px; display: flex; flex-direction: column; -webkit-box-pack: center; justify-content: center; align-items: flex-start; min-height: 100%; z-index: 2; width: 420px; box-shadow: rgba(0, 0, 0, 0.75) 0 0 35px -5px; margin-left: 5%; background: transparent; } .login-page .login-container .login-form { display: flex; flex-direction: column; -webkit-box-align: center; align-items: center; position: absolute; top: 0px; width: 100%; min-height: 100%; max-width: 420px; background-color: #fff; background-color: var(--white); -webkit-backdrop-filter: blur(var(--blur-radius)); backdrop-filter: blur(var(--blur-radius)); background-color: rgba(244, 245, 247, var(--blur-opacity)); } .login-page .login-container .login-form .brand { display: flex; -webkit-box-align: center; align-items: center; margin: 50px auto 100px 50px; color: #525461; color: var(--default); justify-content: center; } .login-page .login-container .login-form .brand .icon { width: 50px; height: auto; margin-right: 25px; } .login-page .login-container .login-form .brand .brand-text { font-size: 1.25rem; font-weight: 700; font-family: "TypoGraphica"; } .login-page .login-container .login-form .brand:hover { text-decoration: none; } .login-page .login-container .login-form .form-login { width: 100%; padding: 20px 50px; box-sizing: border-box; } .login-page .login-container .login-form .form-login .errorbox { text-align: center; color: #fb6340; color: var(--warning); padding-bottom: 2rem; } .login-page .login-container .login-form .form-login .input-group { margin-bottom: 1.25rem; position: relative; } .login-page .login-container .login-form .form-login .input-group::before { font-family: "argon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #525461; color: var(--default); font-size: 1.5rem; position: absolute; z-index: 100; left: 10px; top: 10px; } .login-page .login-container .login-form .form-login .input-group .border { position: absolute; width: 100%; height: 1px; bottom: 0; border-bottom: 1px #5e72e4 solid; border-bottom: 1px var(--primary) solid; transform: scaleX(0); transition: transform 0.3s; } .login-page .login-container .login-form .form-login .input-group input { font-size: 1rem; line-height: 1.5em; display: block; width: 100%; padding: 0.5rem 0.75rem 0.5rem 3rem; margin: 0.825rem 0; box-sizing: border-box; transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); color: #525461; color: var(--default); border: 0; border-radius: 0; border-bottom: 1px solid #fff; border-bottom: 1px solid var(--white); background-color: transparent; background-clip: padding-box; box-shadow: 0 3px 2px rgba(233, 236, 239, 0.05); outline: none; } .login-page .login-container .login-form .form-login .input-group input:focus + .border { transform: scaleX(1); } .login-page .login-container .login-form .form-login .input-group .cbi-input-password { margin-bottom: 2rem; position: relative; } .login-page .login-container .login-form .form-login .user-icon::before { content: "\e971"; } .login-page .login-container .login-form .form-login .pass-icon::before { content: "\e910"; } .login-page .login-container .login-form .cbi-button-apply { width: 100% !important; box-shadow: rgba(0, 0, 0, 0.1) 0 0 50px 0; font-weight: 600; font-size: 15px; color: #fff; color: var(--white); text-align: center; width: 100%; cursor: pointer; min-height: 50px; background-color: #5e72e4 !important; background-color: var(--primary) !important; border-radius: 6px; outline: none; border-width: initial; border-style: none; border-color: initial; border-image: initial; padding: 10px 0px; margin: 30px 0px 100px; transition: all 0.3s !important; letter-spacing: 0.8rem; } .login-page .login-container .login-form .cbi-button-apply:hover, .login-page .login-container .login-form .cbi-button-apply :focus { opacity: 0.9; } .login-page .login-container footer { box-sizing: border-box; width: 100%; text-align: center; line-height: 1.6rem; display: flex; justify-content: space-evenly; margin-top: auto; padding: 0px 0px 30px; z-index: 10; color: #525461; color: var(--default); position: absolute; bottom: 0; } .login-page .login-container footer .ftc { position: absolute; bottom: 30px; width: 100%; } .login-page .login-container footer .luci-link { display: block; } header, .main { width: 100%; } footer { font-size: 0.875rem; overflow: hidden; padding: 1rem; text-align: right; white-space: nowrap; color: #aaa; } footer > a { text-decoration: none; color: #aaa; } small { font-size: 90%; line-height: 1.42857143; white-space: normal; } .main { position: relative; top: 0; bottom: 0; overflow-y: auto; height: 100%; display: flex; flex-direction: row; } .main-left { flex-shrink: 0; width: 15rem; height: 100%; background-color: var(--menu-bg-color); box-shadow: rgba(0, 0, 0, 0.75) 0 0 15px -5px; overflow-x: auto; z-index: 100; } .main-left .sidenav-header { padding: 1.5rem 0.5rem; text-align: center; } .main-left .sidenav-header .brand { display: block; font-size: 1.8rem; color: #5e72e4; color: var(--primary); font-family: "TypoGraphica"; text-decoration: none; text-align: center; cursor: default; margin: 0 2rem; } .main-left .sidenav-header .brand .logo { max-width: 100%; height: auto; } .main-left::-webkit-scrollbar { width: 5px; height: 1px; } .main-left::-webkit-scrollbar-thumb { background-color: #f6f9fc; } .main-left::-webkit-scrollbar-track { background-color: #fff; } .main-right { flex-grow: 1; height: 100%; transition: all 0.2s; overflow-x: hidden; overflow-y: auto; display: flex; flex-direction: column; } .main-right > #maincontent { position: relative; z-index: 50; flex: 1; display: flex; flex-direction: column; } .main-right > #maincontent > .container { margin: 0 1.25rem 1rem 1.25rem; flex-grow: 1; } .main-right > #maincontent .Dashboard { color: var(--gray-dark) !important; } .main-right > #maincontent .Dashboard h3 { color: var(--gray-dark); } .main-right > #maincontent .Dashboard p { margin-bottom: 3px; margin-top: 3px; } .main-right > #maincontent .Dashboard hr { border-top: 1px solid #000; } .main-right > #maincontent .Dashboard .dashboard-bg { background-color: #fff; } .main-right > #maincontent .Dashboard .settings-info { padding-top: 1em; padding-bottom: 1em; } .main-right > #maincontent .Dashboard .settings-info p span:nth-child(2) { max-height: 18.5px; top: 4px; } .main-right > #maincontent .Dashboard .settings-info .label { font-size: 0.7rem; padding: 0.2rem 0.6rem; } header { color: #fff; color: var(--header-color); padding: 0; position: relative; } header.bg-primary { background-color: #5e72e4 !important; background-color: var(--primary) !important; } header::after { content: ""; position: absolute; height: 2rem; width: 100%; background-color: #5e72e4 !important; background-color: var(--primary) !important; } header .fill { padding: 0.8rem 0; border-bottom: 0 solid rgba(255, 255, 255, 0.08) !important; display: flex; } header .fill .container { height: 2rem; padding: 0 1.25rem; display: flex; align-items: center; width: 100%; } header .fill .container .flex1 { flex: 1; } header .fill .container .flex1 .showSide { display: none; color: #fff; font-size: 1.4rem; } header .fill .container .flex1 .showSide:hover { text-decoration: none; } header .fill .container .flex1 .brand { font-size: 1.5rem; color: #fff; font-family: "TypoGraphica"; text-decoration: none; padding-left: 1rem; cursor: default; vertical-align: text-bottom; display: none; } header .fill .container .pull-right { float: right; margin-top: 0rem; display: flex; } header .fill .status span { display: inline-block; font-size: 0.875rem; font-weight: bold; padding: 0.3rem 0.8rem; white-space: nowrap; text-decoration: none; text-transform: uppercase; text-shadow: none; border-radius: 4px; cursor: pointer; transition: all 0.3s; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); margin: 0 0.25rem; } header .fill .status span:last-child { margin-right: 0; } header .fill .status span[data-indicator="poll-status"] { color: #fff; } header .fill .status span[data-style="active"] { background-color: var(--green); } header .fill .status span[data-style="inactive"] { color: #ffffff !important; background-color: #32325d; } #xhr_poll_status { display: flex; margin-left: 0.5rem; } #xhr_poll_status * { color: #fff; } div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] { border: 0 !important; } .danger { background-color: #ff7d60 !important; } .warning { background-color: #f0e68c !important; } .success { background-color: #5cb85c !important; } .notice { background-color: #11cdef !important; color: #fff; } .error { color: #f00; } .alert, .alert-message { font-weight: bold; margin-bottom: 1.25rem; margin-left: 1.25rem; margin-right: 1.25rem; padding: 1rem 1.25rem; border: 0; border-radius: 0.25rem !important; background-color: #fff; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); text-shadow: none; } .alert.error, .alert-message.error { background-color: #ffd600; } .alert h4, .alert-message h4 { padding: 0.25rem 0; border-radius: 4px; background-color: #ffd600; } .alert .btn, .alert-message .btn { height: auto; } .alert-message > h4 { font-size: 110%; font-weight: bold; } .alert-message > * { margin: 0.5rem 0; } .alert-message .btn { padding: 0.3rem 0.6rem; } .container .alert, .container .alert-message { margin-left: 0; margin-right: 0; margin-top: 0rem; } .main .main-left { transition: all 0.2s; } .main .main-left .nav { margin-top: 0.5rem; } .main .main-left .nav > li > a:first-child { display: block; margin: 0.1rem 0.5rem 0.1rem 0.5rem; padding: 0.675rem 0 0.675rem 2.5rem; border-radius: 0.25rem; text-decoration: none; cursor: default; font-size: 1rem; transition: all 0.2s; position: relative; } .main .main-left .nav > li > a:first-child.active { color: #fff; background: #5e72e4; background: var(--primary); } .main .main-left .nav > li > a:first-child.active::before { color: #fff !important; } .main .main-left .nav > li > a:first-child.active::after { transform: rotate(90deg); color: #fff !important; } .main .main-left .nav > li > a:first-child:hover { cursor: pointer; color: #fff; background: #5e72e4; background: var(--primary); } .main .main-left .nav > li > a:first-child:hover::before { color: #fff !important; } .main .main-left .nav > li > a:first-child::before { font-family: "argon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: absolute; left: 0.8rem; padding-top: 3px; transition: all 0.3s; content: "\e915"; color: #5e72e4; color: var(--primary); } .main .main-left .nav li { padding: 0.5rem 1rem; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 0; } .main .main-left .nav li a { display: block; color: #5f6368; color: var(--menu-color); } .main .main-left .nav li.slide { padding: 0; } .main .main-left .nav li.slide ul { display: none; overflow: hidden; } .main .main-left .nav li.slide:hover { background: none; } .main .main-left .nav li.slide .slide-menu { margin: 0 0.5rem 0 2.5rem; padding: 0 0.5rem; } .main .main-left .nav li.slide .slide-menu.active { display: block; } .main .main-left .nav li.slide .slide-menu li { position: relative; border-radius: 0.25rem; margin: 0; background: none; list-style: none; } .main .main-left .nav li.slide .slide-menu li a { text-decoration: none; padding: 0.5rem 0; } .main .main-left .nav li.slide .slide-menu li::after { content: ""; position: absolute; left: 0; bottom: 0; width: 0; height: 2px; background-color: #5e72e4; background-color: var(--primary); transition: all 0.2s; } .main .main-left .nav li.slide .slide-menu li:hover { background: none; } .main .main-left .nav li.slide .slide-menu li:hover::after { width: 100%; } .main .main-left .nav li.slide .slide-menu .active { background: none; color: var(--menu-color); } .main .main-left .nav li.slide .slide-menu .active a { color: var(--menu-color); } .main .main-left .nav li.slide .slide-menu .active::after { content: ""; position: absolute; left: 0; bottom: 0; width: 100%; height: 2px; background-color: #5e72e4; background-color: var(--primary); transition: all 0.2s; } .main .main-left .nav li.slide .slide-menu .active:hover { background: none; } .main .main-left .nav li.slide .slide-menu .active:hover::after { width: 100%; } .main .main-left .nav li .menu { display: block; margin: 0.1rem 0.5rem 0.1rem 0.5rem; padding: 0.675rem 0 0.675rem 2.5rem; border-radius: 0.25rem; text-decoration: none; cursor: default; font-size: 1rem; transition: all 0.2s; position: relative; } .main .main-left .nav li .menu.active { color: #fff; background: #5e72e4; background: var(--primary); } .main .main-left .nav li .menu.active::before { color: #fff !important; } .main .main-left .nav li .menu.active::after { transform: rotate(90deg); color: #fff !important; } .main .main-left .nav li .menu:hover { cursor: pointer; color: #fff; background: #5e72e4; background: var(--primary); } .main .main-left .nav li .menu:hover::before { color: #fff !important; } .main .main-left .nav li .menu::before { font-family: "argon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: absolute; left: 0.8rem; padding-top: 3px; transition: all 0.3s; content: "\e915"; color: #5e72e4; color: var(--primary); } .main .main-left .nav li .menu::after { position: absolute; right: 0.5rem; top: 0.8rem; font-family: "argon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -moz-osx-font-smoothing: grayscale; content: "\e90f"; transition: all 0.15s ease; color: #ced4da; text-rendering: auto; -webkit-font-smoothing: antialiased; transition: all 0.3s; } .main .main-left .nav li .menu[data-title="Status"]:before { content: "\e906"; color: #5e72e4; color: var(--primary); } .main .main-left .nav li .menu[data-title="System"]:before { content: "\e90a"; color: #fb6340; } .main .main-left .nav li .menu[data-title="Services"]:before { content: "\e909"; color: #11cdef; } .main .main-left .nav li .menu[data-title="NAS"]:before { content: "\e90c"; color: #f3a4b5; } .main .main-left .nav li .menu[data-title="VPN"]:before { content: "\e90b"; color: #8965e0; } .main .main-left .nav li .menu[data-title="Network"]:before { content: "\e908"; color: #8965e0; } .main .main-left .nav li .menu[data-title="Bandwidth_Monitor"]:before { content: "\e90d"; color: #2dce89; } .main .main-left .nav li .menu[data-title="Docker"]:before { content: "\e911"; color: #6699ff; } .main .main-left .nav li .menu[data-title="Statistics"]:before { content: "\e913"; color: #8965e0; } .main .main-left .nav li .menu[data-title="Control"]:before { content: "\e912"; color: #5e72e4; color: var(--primary); } .main .main-left .nav li .menu[data-title="Asterisk"]:before { content: "\e914"; color: #fb6340; } .main .main-left .nav li a[data-title="Log_out"]::before, .main .main-left .nav li .food[data-title="Log_out"]::before { content: "\e907"; color: #adb5bd; } .lg { margin: 0; padding: 0 !important; } .logout { display: block; margin: 0.8rem 0.5rem 0.1rem 0.5rem; padding: 0.675rem 0 0.675rem 2.5rem; border-radius: 0.25rem; text-decoration: none; font-size: 1rem; transition: all 0.2s; position: relative; } .logout:before { font-family: "argon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: absolute; left: 0.8rem; padding-top: 3px; transition: all 0.3s; content: "\e907"; color: #32325d !important; } body[class*="node-"] > .main > .main-left > .nav > .slide > .menu::before { transition: transform 0.1s ease-in-out; } body[class*="node-"] > .main > .main-left > .nav > .slide > .menu.active::before { transition: transform 0.2s ease-in-out; } .main > .main-left[style*="overflow: hidden"] > .nav > .slide > .menu::before { display: none; } .cbi-section, .cbi-section-error, #iptables, .Firewall form, #cbi-network > .cbi-section-node, #cbi-wireless > .cbi-section-node, #cbi-wireless > #wifi_assoclist_table, [data-tab-title], [data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), [data-page="admin-system-opkg"] #maincontent > .container { font-family: inherit; font-weight: normal; font-style: normal; line-height: normal; min-width: inherit; margin: 1.25rem 0; padding: 0; border: 0; border-radius: 0.25rem; background-color: #fff; box-shadow: 0 0 1rem 0 rgba(136, 152, 170, 0.15); } .cbi-section:last-child, .cbi-section-error:last-child, #iptables:last-child, .Firewall form:last-child, #cbi-network > .cbi-section-node:last-child, #cbi-wireless > .cbi-section-node:last-child, #cbi-wireless > #wifi_assoclist_table:last-child, [data-tab-title]:last-child, [data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear):last-child, [data-page="admin-system-opkg"] #maincontent > .container:last-child { margin: 0; border: 0; } .cbi-modal .cbi-section, .cbi-section .cbi-section { padding: 0; box-shadow: none; } .cbi-modal .cbi-tabmenu { margin-left: 0; } .cbi-map:not(:first-child) { margin-top: 1rem; } .cbi-map-descr { font-size: small; line-height: 1.5; padding: 0 1.25rem 1rem 1.25rem; } .cbi-section > .cbi-section-descr { padding-top: 1rem !important; padding-bottom: 1rem !important; } .cbi-section > .cbi-section-descr:empty { padding-top: 0 !important; padding-bottom: 0rem !important; } .cbi-section-descr:not(:empty) { font-size: small; line-height: 1.5; padding: 0rem 1rem; } .cbi-map-descr + fieldset { margin-top: 1rem; } .cbi-map-descr > abbr { cursor: help; text-decoration: underline; } .cbi-section > legend { display: none !important; } fieldset > fieldset, .cbi-section > .cbi-section { margin: 0; padding: 0; border: 0; box-shadow: none; } .cbi-section > h3:first-child, .panel-title { font-size: 1.1rem; line-height: 1; display: block; width: 100%; margin: 0; margin-bottom: 0; padding: 0.8755rem 1.25rem; color: #32325d; color: var(--gray-dark); } .cbi-section > h3:first-child, .cbi-section > h4:first-child, .cbi-section > p:first-child, [data-tab-title] > h3:first-child, [data-tab-title] > h4:first-child, [data-tab-title] > p:first-child { padding: 1rem 1.25rem; } .cbi-section p { padding: 1rem; } .cbi-tblsection { overflow-x: auto; } table { border-spacing: 0; border-collapse: collapse; } table, .table { overflow-y: hidden; width: 100%; font-size: 90%; } .table .table-titles th { background-color: #e9ecef; background-color: var(--lighter); } table > tbody > tr > td, table > tbody > tr > th, table > tfoot > tr > td, table > tfoot > tr > th, table > thead > tr > td, table > thead > tr > th, .table > .tbody > .tr > .td, .table > .tbody > .tr > .th, .table > .tfoot > .tr > .td, .table > .tfoot > .tr > .th, .table > .thead > .tr > .td, .table > .thead > .tr > .th, .table > .tr > .td.cbi-value-field, .table > .tr > .th.cbi-section-table-cell { padding: 0.5rem; } .container > .cbi-section:first-of-type > .table[width="100%"] > .tr > .td { padding: 0.6rem; } .cbi-section-table-cell { line-height: 1.1; align-self: flex-end; flex: 1 1 auto; } tr > td, tr > th, .tr > .td, .tr > .th, .cbi-section-table-row::before, #cbi-wireless > #wifi_assoclist_table > .tr:nth-child(2) { border-top: thin solid #ddd; padding: 1.1em 1.25rem; } #cbi-wireless .td, .table[width="100%"] > .tr:first-child > .td, [data-page="admin-network-diagnostics"] .tr > .td, .tr.table-titles > .th, .tr.cbi-section-table-titles > .th { border-top: 0 !important; background-color: #f6f9fc; padding: 1.1em 1.25rem; line-height: 1.3rem; } [data-page="admin-network-network"] .cbi-value-field .cbi-dynlist { padding: 0 !important; } #cbi-network .tr:first-child > .td { border-top: 0; } .table[width="100%"] > .tr:first-child > .td { margin: auto 0; } .cbi-section-table-row { margin-bottom: 1rem; text-align: center !important; background: #f4f4f4; } .cbi-section-table-row:last-child { margin-bottom: 0; } .cbi-section-table-row > .cbi-value-field .cbi-dropdown, .cbi-section-table-row > .cbi-value-field .cbi-input-select, .cbi-section-table-row > .cbi-value-field .cbi-input-text, .cbi-section-table-row > .cbi-value-field .cbi-input-password { width: 100%; } .cbi-section-table-row > .cbi-value-field .cbi-input-text, .cbi-section-table-row > .cbi-value-field .cbi-input-password { min-width: 80px; } .cbi-section-table-row > .cbi-value-field [data-dynlist] > input, .cbi-section-table-row > .cbi-value-field input.cbi-input-password { width: calc(100% - 1.5rem); } .cbi-section-table-row .td { text-align: center !important; } .cbi-section-table-row .td .cbi-checkbox input[type="checkbox"] { margin: 0; } .control-group { display: inline-flex; width: 100%; flex-wrap: wrap; gap: 0px; } .control-group input { border-bottom-right-radius: 0; border-top-right-radius: 0; border-right-width: 0; margin-right: 0; } .control-group input + button { border-bottom-left-radius: 0; border-top-left-radius: 0; margin-left: 0; border-left-width: 0; } .control-group > * { vertical-align: middle; } div > table > tbody > tr:nth-of-type(2n), div > .table > .tr:nth-of-type(2n) { background-color: #f9f9f9; } table table, .table .table, .cbi-value-field table, .cbi-value-field .table, td > table > tbody > tr > td, .td > .table > .tbody > .tr > .td, .cbi-value-field > table > tbody > tr > td, .cbi-value-field > .table > .tbody > .tr > .td { border: 0; } .btn, .cbi-button, .item::after { font-size: 0.875rem; display: inline-block; width: auto !important; padding: 0.5rem 0.75rem; margin-left: 5px; margin-right: 5px; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; transition: all 0.2s ease-in-out; text-align: center; vertical-align: middle; white-space: nowrap; text-decoration: none; border: 0; border-radius: 0.25rem; background-color: #f0f0f0; background-image: none; appearance: none; -ms-touch-action: manipulation; touch-action: manipulation; } .btn:last-child, .cbi-button:last-child { margin-right: 0 !important; } .btn:first-child, .cbi-button:first-child { margin-left: 0 !important; } .btn:only-child, .cbi-button:only-child { margin-left: 5px !important; margin-right: 5px !important; } .btn:not(button) ul:not(.dropdown) li { padding: 0; } .cbi-button-up, .cbi-button-down { display: inline-block; min-width: 0; padding: 0.2rem 1rem; font-size: 0; color: transparent !important; background: url(../icon/arrow.svg) no-repeat center; background-size: 12px 20px; } .cbi-button-up { transform: scaleY(-1); } .cbi-button:not(select) { appearance: none !important; } .btn:hover, .btn:focus, .btn:active, .cbi-button:hover, .cbi-button:focus, .cbi-button:active, .item:hover::after, .item:focus::after, .item:active::after, .cbi-page-actions .cbi-button-apply + .cbi-button-save:hover, .cbi-page-actions .cbi-button-apply + .cbi-button-save:focus, .cbi-page-actions .cbi-button-apply + .cbi-button-save:active { text-decoration: none; outline: 0; } .btn:hover, .btn:focus, .cbi-button:hover, .cbi-button:focus, .item:hover::after, .item:focus::after { box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.2); } .btn:active, .cbi-button:active, .item:active::after { box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); } .cbi-button-up:hover, .cbi-button-up:focus { box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 -2px 2px rgba(0, 0, 0, 0.2); } .cbi-button-up:active { box-shadow: 0 -10px 20px rgba(0, 0, 0, 0.19), 0 -6px 6px rgba(0, 0, 0, 0.23); } .btn:disabled, .cbi-button:disabled { cursor: not-allowed; pointer-events: none; opacity: 0.5; box-shadow: none; } .alert-message [class="btn"], .modal div[class="btn"], .cbi-button-find, .cbi-button-link, .cbi-button-up, .cbi-button-down, .cbi-button-neutral, .cbi-button[name="zero"], .cbi-button[name="restart"], .cbi-button[onclick="hide_empty(this)"] { color: #fff; border: thin solid #8898aa; background-color: #8898aa; } .btn.primary, .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-apply + .cbi-button-save, .cbi-button-add, .cbi-button-save, .cbi-button-positive, .cbi-button-link, .cbi-button[value="Enable"], .cbi-button[value="Scan"], .cbi-button[value^="Back"], .cbi-button-neutral[onclick="handleConfig(event)"] { font-weight: normal; color: #fff !important; border: thin solid #5e72e4; border: thin solid var(--primary); background-color: #5e72e4; background-color: var(--primary); } .cbi-page-actions .cbi-button-apply, .cbi-section-actions .cbi-button-edit, .cbi-button-edit, .cbi-button-apply, .cbi-button-reload, .cbi-button-action, .cbi-button[value="Submit"], .cbi-button[value="Upload"], .cbi-button[value$="Apply"], .cbi-button[onclick="addKey(event)"] { font-weight: normal; color: #fff !important; border: thin solid #5e72e4; border: thin solid var(--primary); background-color: #5e72e4; background-color: var(--primary); } .btn.danger, .cbi-section-remove > .cbi-button, .cbi-button-remove, .cbi-button-reset, .cbi-button-negative, .cbi-button[value="Stop"], .cbi-button[value="Kill"], .cbi-button[onclick="reboot(this)"], .cbi-button-neutral[value="Restart"] { font-weight: normal; color: #fff; border: thin solid #f5365c; border: thin solid var(--red); background-color: #f5365c; background-color: var(--red); } .btn[value="Dismiss"], .cbi-button[value="Terminate"], .cbi-button[value="Reset"], .cbi-button[value="Disabled"], .cbi-button[onclick^="iface_reconnect"], .cbi-button[onclick="handleReset(event)"], .cbi-button-neutral[value="Disable"] { font-weight: normal; color: #fff; border: thin solid #eea236; background-color: #f0ad4e; } .cbi-button-success, .cbi-button-download { font-weight: normal; color: #fff; border: thin solid #4cae4c; background-color: #5cb85c; } .cbi-page-actions .cbi-button-link:first-child { float: left; } .a-to-btn { text-decoration: none; } .cbi-value-field .cbi-button-add { font-weight: bold; padding: 1px 6px; display: inline-block; align-items: center; } .tabs { margin: 0 0 1rem 0; padding: 0 1rem; background-color: #ffffff; border-radius: 0.25rem; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); white-space: nowrap; overflow-x: auto; } .tabs::-webkit-scrollbar { width: 1px; height: 5px; } .tabs::-webkit-scrollbar-thumb { background-color: #f6f9fc; } .tabs::-webkit-scrollbar-track { background-color: #fff; } .tabs li[class~="active"], .tabs li:hover { cursor: pointer; border-bottom: 0.18751rem solid #5e72e4; border-bottom: 0.18751rem solid var(--primary); color: #5e72e4; color: var(--primary); background-color: #e4e9ee; margin-bottom: 0; border-radius: 0; } .tabs li[class~="active"] a, .tabs li:hover a { color: #5e72e4; color: var(--primary); } .tabs li { font-size: 0.875rem; display: inline-block; padding: 0.875rem 0; border-bottom: 0.18751rem solid rgba(0, 0, 0, 0); margin: 0; transition: all 0.2s; } .tabs li a { text-decoration: none; color: #404040; padding: 0.5rem 0.8rem; } .tabs li:hover { border-bottom: 0.18751rem solid #5e72e4; border-bottom: 0.18751rem solid var(--primary); } .cbi-tabmenu { color: white; padding: 0.5rem 1rem 0 1rem; white-space: nowrap; overflow-x: auto; } .cbi-tabmenu::-webkit-scrollbar { width: 1px; height: 5px; } .cbi-tabmenu::-webkit-scrollbar-thumb { background-color: #f6f9fc; } .cbi-tabmenu::-webkit-scrollbar-track { background-color: #fff; } .cbi-tabmenu li { background: #dce3e9; display: inline-block; font-size: 0.875rem; border-top-left-radius: 0.25rem; border-top-right-radius: 0.25rem; padding: 0.5rem 0; border-bottom: 0.18751rem solid rgba(0, 0, 0, 0); margin: 0 0.2rem; } .cbi-tabmenu li a { text-decoration: none; color: #404040; padding: 0.5rem 0.8rem; } .cbi-tabmenu li:hover { cursor: pointer; border-bottom: 0.18751rem solid #5e72e4; border-bottom: 0.18751rem solid var(--primary); color: #5e72e4; color: var(--primary); background-color: #e4e9ee; margin-bottom: 0; } .cbi-tabmenu li:hover a { color: #525f7f; } .cbi-tabmenu li[class~="cbi-tab"] { border-bottom: 0.18751rem solid #5e72e4; border-bottom: 0.18751rem solid var(--primary); color: #5e72e4; color: var(--primary); background-color: #e4e9ee; margin-bottom: 0; } .cbi-tabmenu li[class~="cbi-tab"] a { color: #5e72e4; color: var(--primary); } .cbi-tab-descr { padding: 0.5rem 1.5rem; } .cbi-section-node { padding: 0; } .cbi-section .cbi-section-remove:nth-of-type(2n), .container > .cbi-section .cbi-section-node:nth-of-type(2n) { background-color: #f9f9f9; } [data-tab-title] { overflow: hidden; height: 0; opacity: 0; margin: 0; padding: 0rem 0rem !important; } [data-tab-title] p { margin-left: 1rem; margin-bottom: 1rem; } [data-tab-active="true"] { overflow: visible; height: auto; opacity: 1; transition: opacity 0.25s ease-in; margin: inherit !important; } .cbi-section[id] .cbi-section-remove:nth-of-type(4n + 3), .cbi-section[id] .cbi-section-node:nth-of-type(4n + 4) { background-color: #f9f9f9; } .cbi-section-node-tabbed { margin-top: 0; padding: 0; border: 0 solid #d4d4d4; border-radius: 0.25rem; } .cbi-tabcontainer > .cbi-value:nth-of-type(2n) { background-color: #f9f9f9; } .cbi-value-field { display: table-cell; } .cbi-value-description { line-height: 1.25; display: table-cell; } .cbi-value-description abbr { color: #32325d; color: var(--gray-dark); } .cbi-value-description { font-size: small; padding: 0.5rem; opacity: 0.5; } .cbi-value-title { display: table-cell; float: left; width: 23rem; padding-right: 2rem; text-align: right; word-wrap: break-word; } .cbi-value { display: inline-block; width: 100%; padding: 0.35rem 1rem 0.2rem 1rem; line-height: 2.4rem; } .cbi-value:first-child { padding-top: 1rem; } .cbi-value:last-child { padding-bottom: 1rem; } .cbi-value ul { line-height: 1.25; } .cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"], .cbi-value input[type="password"], .cbi-value textarea { min-width: 18rem; } .cbi-value input[type="password"] { border-bottom-right-radius: 0; border-top-right-radius: 0; font-size: 0.875rem; margin: 0.25rem 0 0.25rem 0.1rem; } .cbi-value input[type="password"] + .cbi-button-neutral { display: flex; align-items: center; justify-content: center; width: 2.5rem !important; padding: 0.5rem 0; margin: 0.25rem 0; font-weight: normal; font-size: 1.2rem; line-height: 1.5rem; color: #fff; outline: 0; background-color: #8898aa; box-shadow: none; border: 1px solid #8898aa; border-radius: 0.25rem; border-top-left-radius: 0; border-bottom-left-radius: 0; } #cbi-firewall-zone .cbi-input-select, #cbi-network-switch_vlan .cbi-input-select { min-width: 11rem; } #cbi-network-switch_vlan .cbi-input-text { max-width: 3rem; } .cbi-input-invalid { color: #f5365c !important; border-color: #f5365c !important; } .cbi-section-error { font-weight: bold; line-height: 1.42857143; margin: 18px; padding: 6px; border: thin solid #f5365c; border-radius: 3px; background-color: #fce6e6; } .cbi-section-error ul { margin: 0 0 0 20px; } .cbi-section-error ul li { font-weight: bold; color: #f5365c; } .td[data-title]::before { font-weight: bold; display: none; padding: 0.25rem 0; content: attr(data-title) ":\20"; text-align: left; white-space: nowrap; } .tr.placeholder .td[data-title]::before { display: none; } .tr[data-title]::before, .tr.cbi-section-table-titles.named::before { font-weight: bold; display: table-cell; align-self: center; flex: 1 1 5%; padding: 0.25rem; content: attr(data-title) "\20"; text-align: center; vertical-align: middle; white-space: normal; word-wrap: break-word; } .cbi-rowstyle-1 { background-color: #f9f9f9; } .cbi-rowstyle-2 { background-color: #eee; } .cbi-rowstyle-2 .cbi-button-up, .cbi-rowstyle-2 .cbi-button-down, body:not(.Interfaces) .cbi-rowstyle-2:first-child { background-color: #fff !important; } .cbi-section-table .cbi-section-table-titles .cbi-section-table-cell { width: auto !important; } .td.cbi-section-actions { text-align: right !important; vertical-align: middle; } .td.cbi-section-actions > * { display: inline-flex; } .td.cbi-section-actions > * > *, .td.cbi-section-actions > * > form > * { margin: 0 5px; display: flex; align-items: center; } .td.cbi-section-actions > * > form { display: inline-flex; margin: 0; } .cbi-checkbox { margin: 0 0.25rem; } .cbi-dynlist { line-height: 1.3; flex-direction: column; min-height: 30px; cursor: text; } .cbi-dynlist > .item { display: inline-flex; flex-wrap: nowrap; margin: 0.25rem 0; position: relative; max-width: 25rem; pointer-events: none; color: #8898aa; outline: 0; } .cbi-dynlist > .item::after { content: "\00D7"; pointer-events: auto; display: flex; align-items: center; justify-content: center; width: 2.5rem !important; margin: 0; font-weight: normal; font-size: 1.2rem; line-height: 1.5rem; color: #fff; border: 1px solid #f5365c; border-radius: 0 0.25rem 0.25rem 0; outline: 0; background-color: var(--red); background-image: none; box-shadow: none; box-sizing: border-box; } .cbi-dynlist > .item > span { display: block; padding: 0.5rem 0.75rem; min-width: 15.5rem; width: 15.5rem; transition: box-shadow 0.15s ease; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; word-break: break-word; font-size: 0.875rem; line-height: 24px; color: #8898aa; border: 1px solid #dee2e6; border-radius: 0.25rem 0 0 0.25rem; outline: 0; background-image: none; box-shadow: none; box-sizing: border-box; } .cbi-dynlist > .add-item { display: inline-flex; align-items: center; width: 100%; min-width: 16rem; margin: 0.25rem 0; gap: 0; flex-wrap: nowrap; } .cbi-dynlist > .add-item input { display: block; padding: 0.5rem 0.75rem; box-sizing: border-box; min-width: 15.5rem; width: 15.5rem; transition: box-shadow 0.15s ease; white-space: nowrap; word-break: break-word; font-size: 0.875rem; line-height: 1.5rem; color: #8898aa; border: 1px solid #dee2e6; border-radius: 0.25rem 0 0 0.25rem; border-right-width: 0; outline: 0; background-image: none; box-shadow: none; } .cbi-dynlist > .add-item .cbi-button { display: flex; width: auto !important; padding-left: 0.8rem; padding-right: 0.8rem; margin-left: 0; align-items: center; justify-content: center; font-size: 0.875rem; line-height: 1.5rem; outline: 0; background-image: none; background-color: var(--gray); box-shadow: none; color: var(--white); border-color: var(--gray); border-radius: 0.25rem; border-top-left-radius: 0; border-bottom-left-radius: 0; } .cbi-dynlist > .add-item .cbi-button-add { width: 2.5rem !important; padding: 0.5rem 0 !important; font-weight: normal; font-size: 1.2rem; color: #fff; background-color: var(--primary); border: 1px solid var(--primary); } .cbi-dynlist > .add-item:not([ondrop]) > input { overflow: hidden; min-width: 15.5rem; width: 15.5rem; white-space: nowrap; text-overflow: ellipsis; } .cbi-dynlist[name="sshkeys"] > .item { max-width: none; } .cbi-dynlist > .cbi-dynlist > .add-item[ondrop] > input { min-width: 13rem; } .cbi-dynlist, .cbi-dropdown { position: relative; display: inline-flex; min-height: 2.1875rem; } .cbi-dropdown[placeholder*="select"] { max-width: 25rem; height: auto; margin-top: -3px; } .cbi-dropdown > ul { display: flex; overflow-x: hidden; overflow-y: auto; width: 100%; margin: 0 !important; padding: 0; list-style: none; outline: 0; } .cbi-dropdown > ul.preview { display: none; } .cbi-button-apply > ul.preview { display: none; } .cbi-button-apply > ul.preview li { color: #fff; } .cbi-button-apply > ul:first-child li { color: #fff; } .cbi-dropdown > .open { flex-basis: 15px; } .cbi-dropdown > .open, .cbi-dropdown > .more { font-size: 1rem; font-weight: 900; line-height: 1em; display: flex; flex-direction: column; flex-grow: 0; flex-shrink: 0; justify-content: center; padding: 0 0.25em; cursor: default; text-align: center; outline: 0; } .cbi-dropdown > .more, .cbi-dropdown > ul > li[placeholder] { font-weight: bold; display: none; color: #777; text-shadow: none; } .cbi-dropdown > ul > li { display: none; overflow: hidden; align-items: center; align-self: center; flex-grow: 1; flex-shrink: 1; min-height: 20px; padding: 0.125rem 0.25em; white-space: nowrap; text-overflow: ellipsis; } .cbi-dropdown > ul > li .hide-open { display: initial; } .cbi-dropdown > ul > li .hide-close { display: none; } .cbi-dropdown > ul > li[display]:not([display="0"]) { border-left: thin solid #ccc; } .cbi-dropdown[empty] > ul { max-width: 1px; } .cbi-dropdown > ul > li > form { display: none; margin: 0; padding: 0; pointer-events: none; } .cbi-dropdown > ul > li img { margin-right: 0.25em; vertical-align: middle; } .cbi-dropdown > ul > li > form > input[type="checkbox"] { height: auto; margin: 0; } .cbi-dropdown > ul > li input[type="text"] { height: 20px; } .cbi-dropdown[open] > ul.dropdown { position: absolute; z-index: 1100; display: block; width: auto; min-width: 100%; max-width: none; max-height: 200px !important; border: 0 solid #918e8c; background: #ffffff; box-shadow: 0 0 4px #918e8c; border-bottom-left-radius: 0.25rem; border-bottom-right-radius: 0.25rem; color: var(--main-menu-color); margin-left: 0 !important; left: 0; } .cbi-dropdown[open] > ul.dropdown li { color: #000; } .cbi-dropdown > ul > li[display], .cbi-dropdown[open] > ul.preview, .cbi-dropdown[open] > ul.dropdown > li, .cbi-dropdown[multiple] > ul > li > label, .cbi-dropdown[multiple][open] > ul.dropdown > li, .cbi-dropdown[multiple][more] > .more, .cbi-dropdown[multiple][empty] > .more { display: flex; align-items: center; flex-grow: 1; } .cbi-dropdown[empty] > ul > li, .cbi-dropdown[optional][open] > ul.dropdown > li[placeholder], .cbi-dropdown[multiple][open] > ul.dropdown > li > form { display: block; } .cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; } .cbi-dropdown[open] > ul.dropdown > li .hide-close { display: initial; } .cbi-dropdown[open] > ul.dropdown > li { border-bottom: thin solid #ccc; padding: 0.5rem 0.8rem; } .cbi-dropdown[open] > ul.dropdown > li label { margin-left: 0.5rem; } .cbi-dropdown[open] > ul.dropdown > li[selected] { background: #e4e9ee; } .cbi-dropdown[open] > ul.dropdown > li.focus { background: #e4e9ee; outline: none; } .cbi-dropdown[open] > ul.dropdown > li:last-child { margin-bottom: 0; border-bottom: 0; } .cbi-dropdown[open] > ul.dropdown > li[unselectable] { opacity: 0.7; } .cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { width: 100%; } .cbi-dropdown[disabled] { pointer-events: none; opacity: 0.6; } .cbi-dropdown .zonebadge { width: 100%; } .cbi-dropdown[open] .zonebadge { width: auto; } .cbi-progressbar { position: relative; display: flex; width: 100%; font-size: 0.75rem; background-color: #e9ecef; border-radius: 0.5rem; height: 1rem; overflow: hidden; } .cbi-progressbar > div { display: block; position: absolute; height: 100%; background-color: var(--bar-bg); border-radius: 0.5rem; transition: width 0.3s; } .cbi-progressbar::after { content: attr(title); position: absolute; font-size: 0.75rem; color: var(--bs-heading-color); width: 100%; height: 100%; text-align: center; line-height: 1rem; z-index: 2; } #modal_overlay { position: fixed; z-index: 900; top: 0; right: 10000px; bottom: 0; left: -10000px; overflow-y: scroll; transition: opacity 0.125s ease-in; opacity: 0; background: rgba(0, 0, 0, 0.7); -webkit-overflow-scrolling: touch; } .modal { display: flex; align-items: center; flex-wrap: wrap; width: 90%; min-width: 270px; max-width: 600px; min-height: 32px; margin: 5em auto; padding: 1rem; border-radius: 0.25rem !important; background: #fff; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); } .modal > * { line-height: normal; flex-basis: 100%; margin-bottom: 0.5em; max-width: 100%; } .modal > pre, .modal > textarea { font-size: 1rem; font-size-adjust: 0.35; overflow: auto; margin-bottom: 0.5em; padding: 8.5px; cursor: auto; white-space: pre-wrap; color: #eee; outline: 0; background-color: #101010; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); } .modal > h4 { display: block; flex-grow: 1; max-width: none; padding: 1rem; margin: -1rem -1rem 0 -1rem; font-size: 1rem; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); border-radius: 0.25rem 0 0 0.25rem; } .modal h5 { margin-top: 1rem; font-weight: 600; } .modal label > input[type="checkbox"] { top: 0; } .modal ul { margin-left: 2.2em; } .modal ul:not(.cbi-tabmenu) { margin-top: 1rem; } .modal ul li { list-style-type: square; color: #808080; } .modal p { word-break: break-word; margin-top: 1rem; } .modal .label { font-size: 0.6rem; font-weight: normal; padding: 0.1rem 0.3rem; padding-bottom: 0; cursor: default; border-radius: 0; } .modal .label.warning { background-color: #f0ad4e !important; } .modal .btn { padding: 0.45rem 0.8rem; } .modal.cbi-modal { max-width: 90%; max-height: none; } body.modal-overlay-active { overflow: hidden; height: 100vh; } body.modal-overlay-active #modal_overlay { right: 0; left: 0; opacity: 1; } .spinning { position: relative; padding-left: 32px !important; } .spinning::before { position: absolute; top: 0; bottom: 0; left: 0.2em; width: 32px; content: ""; background: url(/luci-static/resources/icons/loading.gif) no-repeat center; background-size: 16px; } #view { border-radius: 0.25rem; overflow: hidden; } #view > .spinning { position: fixed; top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%); padding: 1rem; border-radius: 0.5rem; background: #ffffff; box-shadow: 0 0 1rem 0 rgba(136, 152, 170, 0.15); } .hidden { display: none; } .left, .left::before { text-align: left !important; } .right, .right::before { text-align: right !important; } .center, .center::before { text-align: center !important; } .top { align-self: flex-start !important; vertical-align: top !important; } .bottom { align-self: flex-end !important; vertical-align: bottom !important; } .inline { display: inline; } .cbi-page-actions { padding: 1rem; text-align: right; justify-content: flex-end; } .cbi-page-actions > form[method="post"] { display: inline-block; } .th[data-type="button"], .td[data-type="button"], .th[data-type="fvalue"], .td[data-type="fvalue"] { flex: 1 1 2em; text-align: center; } .ifacebadge { display: inline-flex; align-items: center; gap: 0.2rem; padding: 0.25rem 0.8rem; background: #eee; border-radius: 4px; } td > .ifacebadge, .td > .ifacebadge { font-size: 0.875rem; background-color: #f0f0f0; } .ifacebadge > em, .ifacebadge > img { display: inline-block; margin: 0 0.75rem; } .ifacebadge > img + img { margin: 0 0.2rem 0 0; } .network-status-table { display: flex; flex-wrap: wrap; } .network-status-table .ifacebox { flex-grow: 1; border-radius: 0.25rem; overflow: hidden; margin: 1rem; } .network-status-table .ifacebox-body { display: flex; flex-direction: column; height: 100%; gap: 0.5em; } .network-status-table .ifacebox-body > span { flex: 10 10 auto; } .network-status-table .ifacebox-body > div { display: flex; flex-wrap: wrap; gap: 0.5rem; height: 100%; } .network-status-table .ifacebox-body .ifacebadge { align-items: center; flex: 1 1 auto; min-width: 220px; padding: 0.5em; background-color: #fff; } .network-status-table .ifacebox-body .ifacebadge > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cbi-input-textarea { font-family: inherit; width: 100%; font-size: 0.875rem; min-height: 14rem; padding: 0.8rem; color: #8898aa; border-radius: 0.25rem; border: 1px solid #dee2e6; min-width: 16rem; } #content_syslog { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); } #syslog { font-size: small; font-family: "argon"; line-height: 1.25; overflow-y: hidden; width: 100%; min-height: 15rem; padding: 1rem; resize: none; color: #242424; border: 0; border-radius: 0.25rem; background-color: #ffffff; } #syslog:focus { outline: 0; } .uci-change-list { font-family: inherit; overflow: scroll; width: 100%; display: flex; flex-direction: column; flex-wrap: wrap; } .uci-change-list ins, .uci-change-legend-label ins { display: block; padding: 2px; text-decoration: none; border: thin solid #0f0; background-color: #cfc; } .uci-change-list del, .uci-change-legend-label del { font-style: normal; display: block; padding: 2px; text-decoration: none; border: thin solid #f00; background-color: #fcc; } .uci-change-list var, .uci-change-legend-label var { font-style: normal; display: block; padding: 2px; text-decoration: none; border: thin solid #ccc; background-color: #eee; } .uci-change-list var ins, .uci-change-list var del { font-style: normal; padding: 0; white-space: pre; border: 0; } .uci-change-legend { padding: 5px; } .uci-change-legend-label { float: left; width: 150px; } .uci-change-legend-label > ins, .uci-change-legend-label > del, .uci-change-legend-label > var { display: block; float: left; width: 10px; height: 10px; margin-right: 4px; } .uci-change-legend-label var ins, .uci-change-legend-label var del { line-height: 0.4; border: 0; } .uci-change-list var, .uci-change-list del, .uci-change-list ins { padding: 0.5rem; } .uci-dialog .cbi-section { padding: 0.5rem; } .uci-dialog .cbi-section .uci-change-legend { line-height: 15px; padding: 10px 20px 0 20px; } .uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label { padding: 0; margin: 0; position: relative; float: none; display: inline-block; width: 25%; } .uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > ins, .uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > del { width: 14px; height: 14px; } .uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var { position: relative; width: 14px; height: 14px; } .uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var ins, .uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var del { position: absolute; left: 2px; top: 2px; right: 2px; bottom: 2px; } .uci-dialog .cbi-section .uci-change-list { overflow: auto; } .uci-dialog .cbi-section .uci-change-list + .right .btn { color: #333; } .uci-dialog .cbi-section .uci-change-list + .right .cbi-dropdown ul:not(.dropdown) li { color: #fff; } .uci-dialog .cbi-section .uci-change-list + .right .cbi-button { padding: 0.45rem 0.8rem; } #iwsvg, #iwsvg2, #bwsvg { border: thin solid #d4d4d4 !important; } #iwsvg, [data-page="admin-status-realtime-bandwidth"] #bwsvg { border-top: 0 !important; } .ifacebox { line-height: 1.25; display: inline-flex; overflow: hidden; flex-direction: column; border-radius: 4px; min-width: 100px; background-color: #f9f9f9; } .ifacebox-head { padding: 0.25em; background: #eee; } .ifacebox-head.active { background: #5e72e4; background: var(--primary); } .ifacebox-head.active * { color: #fff; color: var(--white); } .ifacebox-body { padding: 0.875rem 1rem; line-height: 1.6em; } .cbi-image-button { margin-left: 0.5rem; } .zonebadge { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; } .zonebadge .ifacebadge { margin: 0.1rem 0.2rem; padding: 0.2rem 0.3rem; border: thin solid #6c6c6c; } .zonebadge > input[type="text"] { min-width: 10rem; margin-top: 0.3rem; padding: 0.16rem 1rem; } .zonebadge > em, .zonebadge > strong { display: inline-block; margin: 0 0.2rem; } .cbi-value-field .cbi-input-checkbox, .cbi-value-field .cbi-input-radio { margin-top: 0.1rem; } .cbi-value-field > ul > li { display: flex; } .cbi-value-field > ul > li > label { margin-top: 0.5rem; } .cbi-value-field > ul > li .ifacebadge { margin-top: -0.5rem; margin-left: 0.4rem; background-color: #eee; } .cbi-section-table-row > .cbi-value-field .cbi-dropdown { min-width: 3rem; } .cbi-section-create { display: inline-flex; align-items: center; padding: 0.5rem 1rem; } .cbi-section-remove { padding: 0.5rem 1rem; } div.cbi-value var, td.cbi-value-field var, .td.cbi-value-field var { font-style: italic; color: #0069d6; } .cbi-optionals { padding: 1rem 1rem 0 1rem; border-top: thin solid #ccc; } .cbi-dropdown-container { position: relative; } .cbi-tooltip-container, span[data-tooltip], span[data-tooltip] .label { cursor: help !important; } .cbi-tooltip { position: absolute; z-index: 1000; left: -1000px; padding: 2px 5px; transition: opacity 0.25s ease-out; white-space: pre; pointer-events: none; opacity: 0; border-radius: 3px; background: #fff; box-shadow: 0 0 2px #444; } .cbi-tooltip-container:hover .cbi-tooltip { left: auto; transition: opacity 0.25s ease-in; opacity: 1; } .zonebadge .cbi-tooltip { margin: -1.5rem 0 0 -0.5rem; padding: 0.25rem; background: inherit; } .zonebadge-empty { color: #404040; background: repeating-linear-gradient( 45deg, rgba(204, 204, 204, 0.5), rgba(204, 204, 204, 0.5) 5px, rgba(255, 255, 255, 0.5) 5px, rgba(255, 255, 255, 0.5) 10px ); } .zone-forwards { display: flex; min-width: 10rem; } .zone-forwards > * { flex: 1 1 45%; } .zone-forwards > span { flex-basis: 10%; padding: 0 0.25rem; text-align: center; } .zone-forwards .zone-src, .zone-forwards .zone-dest { display: flex; flex-direction: column; } .label { font-size: 0.875rem; font-weight: bold; padding: 0.3rem 0.8rem; white-space: nowrap; text-decoration: none; text-transform: uppercase; color: #fff !important; border-radius: 3px; background-color: #bfbfbf; text-shadow: none; } label > input[type="checkbox"], label > input[type="radio"] { position: relative; top: 0.4rem; right: 0.2rem; margin: 0; vertical-align: bottom; } label[data-index][data-depends] { padding-right: 2em; } .showSide { display: none; } .darkMask { position: fixed; z-index: 99; display: none; width: 100%; height: 100%; content: ""; top: 0; background-color: rgba(0, 0, 0, 0.56); transition: all 0.2s; } .darkMask.active { display: block; } #diag-rc-output > pre, #command-rc-output > pre, [data-page="admin-services-wol"] .notice code { font-size: 1.2rem; font-size-adjust: 0.35; line-height: normal; display: block; overflow-y: hidden; width: 100%; padding: 8.5px; white-space: pre; color: #eee; background-color: #101010; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); } [data-page="admin-network-diagnostics"] .table { box-shadow: none; } [data-page="admin-network-diagnostics"] .cbi-section { padding: 1rem; font-family: monospace; background: #fff !important; } [data-page="admin-network-diagnostics"] textarea { background: transparent; border-radius: 0.25rem; font-family: "argon" !important; color: #8898aa; border: 1px solid #dee2e6; padding: 0.5rem; } [data-page="admin-network-diagnostics"] .tr > .td { background-color: #fff !important; border-bottom: 1px solid #dee2e6 !important; } input[name="ping"], input[name="traceroute"], input[name="nslookup"] { width: 80%; } .node-status-overview > .main fieldset:nth-child(4) .td:nth-child(2), .node-status-processes > .main .table .tr .td:nth-child(3) { white-space: normal; } div[style*="display:grid;grid-template-columns:repeat"] { display: flex !important; justify-content: space-evenly !important; padding-bottom: 1rem; flex-wrap: wrap; font-family: "argon"; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox { text-align: center; flex-basis: 100px; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body { font-size: 0.7rem; padding: 0.875rem; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body .cbi-tooltip-container { font-size: inherit !important; } @media screen and (max-width: 484px) { div[style*="display:grid;grid-template-columns:repeat"] .ifacebox { flex-basis: 80px; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body { padding: 0.875rem 0.5rem; font-size: 0.6rem; } } [data-page="admin-system-attendedsysupgrade"] #view .cbi-button { margin-left: 0 !important; margin-top: 1rem !important; } [data-page="admin-system-reboot"] p { padding-left: 1.5rem; } [data-page="admin-system-reboot"] p > span { position: relative; top: 0.1rem; left: 1rem; } [data-page="admin-system-reboot"] .cbi-button { background: #fb6340 !important; border-color: #fb6340 !important; margin-left: 0 !important; } [data-page="admin-system-reboot"] #view > h2:first-child + p { margin-bottom: 1rem; } [data-page="admin-system-poweroff"] .container h2 + br + p { margin-bottom: 1rem; padding-left: 1.5rem; } [data-page="admin-vpn-passwall"] h4 { background: transparent; } [data-page="admin-system-filetransfer"] #cbi-upload { margin-top: 0; } [data-page="admin-system-filetransfer"] .cbi-section-table { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); } #cbi-samba [data-tab="template"] .cbi-value-field { display: block; } #cbi-samba [data-tab="template"] .cbi-value-title { width: auto; padding-bottom: 0.6rem; } [data-page="admin-system-admin"] .cbi-map h2, [data-page="admin-system-admin-password"] .cbi-map h2, [data-page="admin-system-admin"] .cbi-map .cbi-map-descr, [data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr { margin-left: 0; color: #32325d; color: var(--gray-dark); } [data-page="admin-system-admin-sshkeys"] .cbi-dynlist { margin-left: 1rem; } [data-page="admin-system-opkg"] h2 { margin-left: 0; color: #32325d; color: var(--gray-dark); } .controls { margin: 0.5em 1rem 1em 1rem !important; } .controls > * > .btn:not([aria-label$="page"]) { flex-grow: initial !important; margin-top: 0.25rem; } .controls > #pager > .btn[aria-label$="page"] { font-size: 1.4rem; font-weight: bold; } .controls > * > label { margin-bottom: 0.2rem; } [data-page="admin-system-opkg"] div.btn { line-height: 3; display: inline; padding: 0.3rem 0.6rem; } [data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), [data-page="admin-system-opkg"] #maincontent > .container { margin-top: 1rem; padding-top: 0.01rem; } [data-page="admin-system-opkg"] #maincontent > .container { margin: 0 1.25rem 1rem 1.25rem; margin-bottom: 1rem; } .td.version, .td.size { white-space: normal !important; word-break: break-word; } .cbi-tabmenu + .cbi-section { margin-top: 0; } [data-page="admin-system-system"] .control-group { margin-top: 0.5rem; } [data-page="admin-system-system"] .cbi-dynlist { margin: 0.25rem 0; } [data-page="admin-system-startup"] [data-tab-title] p { margin-left: 0; margin-bottom: 0; position: relative; } [data-page="admin-system-startup"] textarea { line-height: 1.25; overflow-y: auto; width: 100%; min-height: 15rem; padding: 1rem; resize: none; color: #8898aa; border-radius: 0.25rem; border: 1px solid #dee2e6; } [data-page="admin-system-startup"] textarea:focus-visible { outline: none; box-shadow: none; border: 1px solid var(--primary); } [data-page="admin-system-crontab"] #view p { margin-bottom: 1rem; } [data-page="admin-system-crontab"] #view p:last-child { margin-bottom: 0; } [data-page="admin-system-crontab"] #view p textarea { line-height: 1.25; overflow-y: hidden; width: 100%; min-height: 15rem; padding: 1rem; resize: none; background-color: transparent; background: var(--white); outline: none; color: #8898aa; border-radius: 0.25rem; border: 1px solid #dee2e6; } [data-page="admin-system-attendedsysupgrade-configuration"] .cbi-map .cbi-map-descr { padding-bottom: 0; } [data-page="admin-system-flash"] .cbi-value { padding: 0; } [data-page="admin-system-flash"] .cbi-section .cbi-section { margin-top: 0; } [data-page="admin-system-flash"] .cbi-map-tabbed { border-radius: 0.25rem; } [data-page="admin-system-flash"] .cbi-section-node { padding-top: 0; padding-bottom: 0.5rem; } [data-page="admin-system-flash"] legend { display: block !important; font-size: 1.2rem; width: 100%; display: block; margin-bottom: 0; padding: 1rem 0 1rem 1.5rem; border-bottom: 1px solid rgba(0, 0, 0, 0.05); line-height: 1.5; margin-bottom: 0rem; letter-spacing: 0.1rem; color: #32325d; font-weight: bold; } [data-page="admin-system-flash"] .cbi-section-descr { font-weight: 600; padding: 1rem 0 1rem 1.5rem; color: #525f7f; } [data-page="admin-system-flash"] .cbi-page-actions { padding: 0rem 1rem 1rem 0rem; } [data-page="admin-system-flash"] .modal label > input[type="checkbox"] { top: -0.25rem; } [data-page="admin-system-flash"] .modal .btn { white-space: normal !important; } #cbi-wireless > #wifi_assoclist_table > .tr { box-shadow: inset 1px -1px 0 #ddd, inset -1px -1px 0 #ddd; } #cbi-wireless > #wifi_assoclist_table > .tr.placeholder > .td { right: 33px; bottom: 33px; left: 33px; border-top: thin solid #ddd !important; } #cbi-wireless > #wifi_assoclist_table > .tr.table-titles { box-shadow: inset 1px 0 0 #ddd, inset -1px 0 0 #ddd; } #cbi-wireless > #wifi_assoclist_table > .tr.table-titles > .th { border-bottom: thin solid #ddd; box-shadow: 0 -1px 0 0 #ddd; } #wifi_assoclist_table > .tr > .td[data-title="RX Rate / TX Rate"] { width: 23rem; } [data-page="admin-network-dhcp"] .cbi-value { padding: 0; } [data-page="admin-network-dhcp"] [data-tab-active="true"] { padding: 1rem 0 !important; } #iptables { margin: 0; } .Firewall form { margin: 2rem 2rem 0 0; padding: 0; box-shadow: none; } #cbi-firewall-redirect table *, #cbi-network-switch_vlan table *, #cbi-firewall-zone table * { font-size: small; } #cbi-firewall-redirect table input[type="text"], #cbi-network-switch_vlan table input[type="text"], #cbi-firewall-zone table input[type="text"] { width: 5rem; } #cbi-firewall-redirect table select, #cbi-network-switch_vlan table select, #cbi-firewall-zone table select { min-width: 3.5rem; } #cbi-network-switch_vlan .th, #cbi-network-switch_vlan .td { flex-basis: 12%; } #cbi-firewall-zone .table, #cbi-network-switch_vlan .table { display: block; } #cbi-firewall-zone .td, #cbi-network-switch_vlan .td { width: 100%; } [data-page="admin-network-firewall-custom"] #view p, [data-page="admin-status-routes"] #view p { padding: 0 1.5rem; margin-bottom: 1rem; } [data-page="admin-network-firewall-custom"] #view p textarea, [data-page="admin-status-routes"] #view p textarea { padding: 1rem; border-radius: 0.25rem; } [data-page="admin-network-firewall-custom"] #view > h3, [data-page="admin-status-routes"] #view > h3 { border-radius: 0.25rem 0.25rem 0 0; } #applyreboot-container { margin: 2rem; } #applyreboot-section { line-height: 300%; margin: 2rem; } .OpenVPN a { line-height: initial !important; } .commandbox { width: 24% !important; margin: 10px 0 0 10px !important; padding: 0.5rem 1rem; border-bottom: thin solid #ccc; background: #eee; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); } .commandbox h3 { line-height: normal !important; overflow: hidden; margin: 6px 0 !important; white-space: nowrap; text-overflow: ellipsis; } .commandbox div { left: auto !important; } .commandbox code { position: absolute; overflow: hidden; max-width: 60%; margin-left: 4px; padding: 2px 3px; white-space: nowrap; text-overflow: ellipsis; } .commandbox code:hover { overflow-y: auto; max-height: 50px; white-space: normal; } .commandbox p:first-of-type { margin-top: -6px; } .commandbox p:nth-of-type(2) { margin-top: 2px; } [data-page^="admin-system-commands"] .panel-title, [data-page^="command-cfg"] .mobile-hide, [data-page^="command-cfg"] .showSide { display: none; } #command-rc-output .alert-message { line-height: 1.42857143; position: absolute; top: 40px; right: 32px; max-width: 40%; margin: 0; animation: anim-fade-in 1.5s forwards; word-break: break-word; opacity: 0; } @keyframes anim-fade-in { 100% { opacity: 1; } } input[type="checkbox"] { appearance: none !important; -webkit-appearance: none !important; border: 1px solid var(--primary); width: 16px !important; height: 16px !important; padding: 0; cursor: pointer; transition: all 0.2s; margin: 0.75rem 0 0 0; } input[type="checkbox"]:checked { border: 1px solid #5e72e4; border: 1px solid var(--primary); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e") !important; background-color: #5e72e4; background-color: var(--primary); background-size: 70%; background-repeat: no-repeat; background-position: center; } .fb-container .cbi-button { height: auto !important; } #cbi-usb_printer-printer em { display: block; padding: 1rem; text-align: center; } pre.command-output { padding: 1.5rem; } [data-page="admin-nlbw-display"] .cbi-section[data-tab="export"] { padding: 1.5rem !important; } [data-page="admin-nlbw-backup"] form { padding-left: 1.5rem; } [data-page="admin-status-iptables"] .right { margin-bottom: 0 !important; } [data-page="admin-services-ttyd"] .container { display: flex; flex-direction: column; } [data-page="admin-services-ttyd"] #view { flex: 1; } [data-page="admin-services-ttyd"] #view iframe { height: 100%; } [data-page="admin-system-fileassistant"] .fb-container .panel-title { padding: 0.5rem 0.75rem !important; } [data-page="admin-system-fileassistant"] .cbi-section.fb-container { padding: 0.5rem; } [data-page="admin-system-fileassistant"] .fb-container .panel-container { border-bottom-color: #dee2e6; } [data-page^="admin-services-openclash"] .cbi-tabmenu > li { border-right: none !important; margin: 0 0.4rem 0 0 !important; } [data-page^="admin-services-openclash"] .cbi-tabmenu > li:last-child { margin-right: 0 !important; } [data-page^="admin-services-openclash"] #tab-content .dom { padding: 0 1rem 1rem 1rem; } [data-page^="admin-services-openclash"] .cbi-input-file { padding: 0.2813rem; box-sizing: content-box; width: 15rem !important; } [data-page^="admin-services-openclash"] [id="container.openclash.config.debug"] fieldset { border: none !important; padding: 1rem !important; } [data-page^="admin-services-openclash"] #diag-rc-output > pre, [data-page^="admin-services-openclash"] #dns-rc-output > pre { font-size: 0.875rem; color: #8898aa; border: 1px solid #dee2e6; background-color: transparent; border-radius: 0.25rem; font-family: "argon" !important; box-shadow: none; } [data-page^="admin-services-openclash"] #debug-rc-output > textarea { font-family: "argon" !important; } [data-page^="admin-services-openclash"] .CodeMirror { font-size: inherit; font-family: "argon" !important; } [data-page^="admin-services-openclash"] .cbi-button-up, [data-page^="admin-services-openclash"] .cbi-button-down { padding: 0.8rem 1.5rem; background-color: #f1f1f1; font-size: 0; } [data-page^="admin-services-openclash"] select#CORE_VERSION, [data-page^="admin-services-openclash"] select#RELEASE_BRANCH { width: auto; } @media all and (-ms-high-contrast: none) { .main > .main-left > .nav > .slide > .menu::before { top: 30.25%; } .main > .main-left > .nav > li:last-child::before { top: 20%; } .showSide::before { top: -12px; } } @media screen and (max-width: 1600px) { header > .fill > .container > #logo { margin: 0 2.5rem 0 0.5rem; } .main-left { width: calc(0% + 13rem); } .btn:not(button), .label { padding: 0.5rem 0.75rem; } .cbi-value-title { width: 15rem; padding-right: 0.6rem; } .cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"], .cbi-value input[type="password"], .cbi-value textarea { min-width: 18rem; } #cbi-firewall-zone .cbi-input-select { min-width: 9rem; } .cbi-input-textarea { font-size: small; } .node-admin-status > .main fieldset li > a { padding: 0.3rem 0.6rem; } } @media screen and (max-width: 1366px) { header > .fill > .container { cursor: default; } .main-left { width: calc(0% + 13rem); } .tabs > li > a, .cbi-tabmenu > li > a { padding: 0.2rem 0.8rem; } .panel-title { font-size: 1.1rem; padding-bottom: 1rem; } table { font-size: 0.875rem !important; width: 100% !important; } .table .cbi-input-text { width: 100%; } .cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"], .cbi-value input[type="password"] { min-width: 16rem; } #cbi-firewall-zone .cbi-input-select { min-width: 4rem; } .main > .main-left > .nav > li, .main > .main-left > .nav > li > a, .main .main-left .nav > li > a:first-child, .main > .main-left > .nav > .slide > .menu, .main > .main-left > .nav > li > [data-title="Log_out"] { font-size: 0.9rem; } .main > .main-left > .nav > .slide > .slide-menu > li > a { font-size: 0.875rem; } #modal_overlay { top: 0rem; } [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) { display: block; } [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table), [data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table), [data-page="admin-network-hosts"] .table, [data-page="admin-network-routes"] .table { overflow-y: visible; } .commandbox { width: 32% !important; } .btn:not(button), .cbi-button { font-size: 0.875rem; } } @media screen and (max-width: 1152px) { header > .fill > .container > #logo { display: none; } header > .fill > .container > .brand { position: relative; } html, .main { overflow-y: visible; } .main > .loading > span { top: 25%; } .main-left { width: calc(0% + 13rem); } body:not(.logged-in) .showSide { visibility: hidden; width: 0; margin: 0; } .node-main-login > .main .cbi-value-title { text-align: left; } .cbi-value-title { width: 12rem; padding-right: 1rem; } .cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"] { width: 16rem; min-width: 16rem; } .cbi-value input[name^="pw"], .cbi-value input[data-update="change"]:nth-child(2) { width: 13rem !important; min-width: 13rem; } #diag-rc-output > pre, #command-rc-output > pre, [data-page="admin-services-wol"] .notice code { font-size: 1rem; } .table { display: block; } .Interfaces .table { overflow-x: hidden; } #packages.table { display: grid; } .tr { display: flex; flex-direction: row; flex-wrap: wrap; } .Overview .table[width="100%"] > .tr { flex-wrap: nowrap; } .tr.placeholder { border-bottom: thin solid #ddd; } .tr.placeholder > .td, #cbi-firewall .tr > .td, #cbi-network .tr:nth-child(2) > .td, .cbi-section #wifi_assoclist_table .tr > .td { border-top: 0; } .th, .td { display: inline-block; align-self: flex-start; flex: 2 2 10%; text-overflow: ellipsis; word-wrap: break-word; } .td select, .td input[type="text"] { width: 100%; word-wrap: normal; } .td [data-dynlist] > input, .td input.cbi-input-password { width: calc(100% - 1.5rem); } .td[data-type="button"], .td[data-type="fvalue"] { flex: 1 1 12.5%; text-align: left; } .th.cbi-value-field, .td.cbi-value-field, .th.cbi-section-table-cell, .td.cbi-section-table-cell { flex-basis: auto; padding-top: 1rem; } .cbi-section-table-row { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); } .td.cbi-value-field, .cbi-section-table-cell { display: inline-block; flex: 10 10 auto; flex-basis: 50%; text-align: center; } .td.cbi-section-actions { vertical-align: bottom; } .tr.table-titles, .tr.cbi-section-table-titles, .tr.cbi-section-table-descr { display: none; } .tr[data-title]::before, .tr.cbi-section-table-titles.named::before { font-size: 0.9rem; display: block; flex: 1 1 100%; border-bottom: thin solid rgba(0, 0, 0, 0.26); background: #e9ecef; } .td[data-title], [data-page^="admin-status-realtime"] .td[id] { text-align: left; } .td[data-title]::before { display: block; } .cbi-button + .cbi-button { margin-left: 0; } .td.cbi-section-actions > * > *, .td.cbi-section-actions > * > form > * { margin: 2.1px 3px; } .Firewall form { position: static !important; margin: 0 0 2rem 0; padding: 2rem; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); } .Firewall form input { width: 100% !important; margin: 0; margin-top: 1rem; } .Firewall .center, .Firewall .center::before { text-align: left !important; } .commandbox { width: 100% !important; margin-left: 0 !important; } .btn:not(button), .cbi-button { font-size: 0.875rem; } } @media screen and (max-width: 768px) { body { font-size: 0.875rem; } .cbi-progressbar::after { font-size: 0.6rem; } .main-left { position: fixed; z-index: 100; width: 0; } .main-left.active { width: 13rem; } .main-right { width: 100%; } .main-right.active { overflow-y: hidden; } .darkMask.active { display: block; } .showSide { padding: 0.1rem; position: relative; z-index: 99; display: inline-block !important; } .showSide::before { font-family: "argon" !important; font-style: normal !important; font-weight: normal !important; font-variant: normal !important; text-transform: none !important; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; content: "\e20e"; font-size: 1.7rem; } header > .fill > .container > .flex1 > .brand { display: inline-block; } .main > .main-left > .nav > .slide > .slide-menu > li > a { font-size: 0.875rem; } } @media screen and (max-width: 600px) { .mobile-hide { display: none; } #maincontent > .container { margin: 0 1rem 1rem 1rem; } .cbi-value-title { text-align: left; } [data-page="admin-system-flash"] legend { padding: 1rem 0 1rem 1rem; } [data-page="admin-system-flash"] .cbi-section-descr { padding: 1rem 0 1rem 1rem; } [data-page="admin-system-flash"] .cbi-value { padding: 0 1rem; } [data-page="admin-network-dhcp"] [data-tab-active="true"] { padding: 1rem 1rem !important; } .cbi-dynlist p { padding: 0.5rem 1rem; } body { overflow-x: hidden; } .node-main-login .main .main-right #maincontent .container .cbi-map .cbi-section .cbi-section-node .cbi-value .cbi-value-field { width: 16rem; } .node-main-login footer { display: none; } .tabs::-webkit-scrollbar, .cbi-tabmenu::-webkit-scrollbar { width: 0px; height: 0px; } .cbi-value-field, .cbi-value-description { display: block !important; padding-left: 0 !important; padding-right: 0 !important; } [data-page="admin-system-admin-password"] .cbi-value-field { display: table-cell !important; } .modal.cbi-modal { max-width: 100%; max-height: none; } .modal { display: flex; align-items: center; flex-wrap: wrap; width: 100%; min-width: 270px; max-width: 600px; min-height: 32px; margin: 5em auto; padding: 1em; border-radius: 3px !important; background: #fff; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); } .cbi-dropdown[open] > ul.dropdown { margin-bottom: 1rem; } .login-page .login-container footer { display: none; } } @media screen and (min-width: 600px) { ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar, ::-webkit-scrollbar-corner { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: var(--primary); } ::-webkit-scrollbar-thumb:active { background: var(--primary); } } @media screen and (max-width: 480px) { .mobile-hide { display: none; } .login-page .login-container { margin-left: 0rem !important; width: 100%; } .login-page .login-container .login-form .form-login .input-group::before { color: #525461; } .login-page .login-container .login-form .form-login .input-group input { color: #525461; border-bottom: white 1px solid; border-bottom: var(--white) 1px solid; border-radius: 0; } } ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/routerich/css/dark.css ================================================ /**/ ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/routerich/css/routerich.css ================================================ /* ! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com */ /* 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) */ *, ::before, ::after { box-sizing: border-box; /* 1 */ border-width: 0; /* 2 */ border-style: solid; /* 2 */ border-color: #e5e7eb; /* 2 */ } ::before, ::after { --tw-content: ''; } /* 1. Use a consistent sensible line-height in all browsers. 2. Prevent adjustments of font size after orientation changes in iOS. 3. Use a more readable tab size. 4. Use the user's configured `sans` font-family by default. 5. Use the user's configured `sans` font-feature-settings by default. 6. Use the user's configured `sans` font-variation-settings by default. 7. Disable tap highlights on iOS */ html, :host { line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ -moz-tab-size: 4; /* 3 */ tab-size: 4; /* 3 */ font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ font-variation-settings: normal; /* 6 */ -webkit-tap-highlight-color: transparent; /* 7 */ } /* 1. Remove the margin in all browsers. 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. */ body { margin: 0; /* 1 */ line-height: inherit; /* 2 */ } /* 1. Add the correct height in Firefox. 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 3. Ensure horizontal rules are visible by default. */ hr { height: 0; /* 1 */ color: inherit; /* 2 */ border-top-width: 1px; /* 3 */ } /* Add the correct text decoration in Chrome, Edge, and Safari. */ abbr:where([title]) { text-decoration: underline dotted; } /* Remove the default font size and weight for headings. */ h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } /* Reset links to optimize for opt-in styling instead of opt-out. */ a { color: inherit; text-decoration: inherit; } /* Add the correct font weight in Edge and Safari. */ b, strong { font-weight: bolder; } /* 1. Use the user's configured `mono` font-family by default. 2. Use the user's configured `mono` font-feature-settings by default. 3. Use the user's configured `mono` font-variation-settings by default. 4. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ font-feature-settings: normal; /* 2 */ font-variation-settings: normal; /* 3 */ font-size: 1em; /* 4 */ } /* Add the correct font size in all browsers. */ small { font-size: 80%; } /* Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 3. Remove gaps between table borders by default. */ table { text-indent: 0; /* 1 */ border-color: inherit; /* 2 */ border-collapse: collapse; /* 3 */ } /* 1. Change the font styles in all browsers. 2. Remove the margin in Firefox and Safari. 3. Remove default padding in all browsers. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-feature-settings: inherit; /* 1 */ font-variation-settings: inherit; /* 1 */ font-size: 100%; /* 1 */ font-weight: inherit; /* 1 */ line-height: inherit; /* 1 */ color: inherit; /* 1 */ margin: 0; /* 2 */ padding: 0; /* 3 */ } /* Remove the inheritance of text transform in Edge and Firefox. */ button, select { text-transform: none; } /* 1. Correct the inability to style clickable types in iOS and Safari. 2. Remove default button styles. */ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; /* 1 */ background-color: transparent; /* 2 */ background-image: none; /* 2 */ } /* Use the modern Firefox focus style for all focusable elements. */ :-moz-focusring { outline: auto; } /* Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) */ :-moz-ui-invalid { box-shadow: none; } /* Add the correct vertical alignment in Chrome and Firefox. */ progress { vertical-align: baseline; } /* Correct the cursor style of increment and decrement buttons in Safari. */ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } /* 1. Correct the odd appearance in Chrome and Safari. 2. Correct the outline style in Safari. */ [type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /* Remove the inner padding in Chrome and Safari on macOS. */ ::-webkit-search-decoration { -webkit-appearance: none; } /* 1. Correct the inability to style clickable types in iOS and Safari. 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Add the correct display in Chrome and Safari. */ summary { display: list-item; } /* Removes the default spacing and border for appropriate elements. */ blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; } fieldset { margin: 0; padding: 0; } legend { padding: 0; } ol, ul, menu { list-style: none; margin: 0; padding: 0; } /* Reset default styling for dialogs. */ dialog { padding: 0; } /* Prevent resizing textareas horizontally by default. */ textarea { resize: vertical; } /* 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 2. Set the default placeholder color to the user's configured gray 400 color. */ input::placeholder, textarea::placeholder { opacity: 1; /* 1 */ color: #9ca3af; /* 2 */ } /* Set the default cursor for buttons. */ button, [role="button"] { cursor: pointer; } /* Make sure disabled buttons don't get the pointer cursor. */ :disabled { cursor: default; } /* 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) This can trigger a poorly considered lint error in some tools but is included by design. */ img, svg, video, canvas, audio, iframe, embed, object { display: block; /* 1 */ vertical-align: middle; /* 2 */ } /* Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) */ img, video { max-width: 100%; height: auto; } /* Make elements with the HTML hidden attribute stay hidden by default */ [hidden] { display: none; } *, ::before, ::after{ --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; } ::backdrop{ --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; } body{ --tw-bg-opacity: 1; background-color: rgb(226 232 240 / var(--tw-bg-opacity)); font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0em; } body.logged-in{ --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } :root { --logo: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' style='enable-background:new 0 0 380 380' viewBox='0 0 380 380'%3E%3Cpath d='M324.6 186.6c-6.9 0-12.5 5.6-12.5 12.5s5.6 12.5 12.5 12.5 12.5-5.6 12.5-12.5-5.6-12.5-12.5-12.5z'/%3E%3Cpath d='M292.6 241h48.3c8.8 0 15.9-7.1 15.9-15.9v-51.9c0-8.8-7.1-15.9-15.9-15.9h-43.3c2.4 10.7 3.7 21.8 3.7 33.2 0 17.7-3 34.7-8.7 50.5z' style='fill:none'/%3E%3Cpath d='M340.9 135.3h-5.3V51.5c0-6.1-4.9-11-11-11s-11 4.9-11 11v83.8h-22.8c2.8 7.1 5.1 14.4 6.8 22h43.3c8.8 0 15.9 7.1 15.9 15.9V225c0 8.8-7.1 15.9-15.9 15.9h-48.3c-2.7 7.6-6 15-9.9 22h58.2c20.9 0 37.9-17 37.9-37.9v-51.8c0-20.9-17-37.9-37.9-37.9z'/%3E%3Ccircle cx='152.2' cy='190.5' r='150' style='fill:%23ffd400'/%3E%3Cpath d='M101.9 169.6s1.4-2.8 4.8-5.9c1.7-1.5 4-3.1 6.8-4.3 2.8-1.1 6.3-1.8 9.9-1.1l1.3.3.5.1.2.1.5.2 2 .8c1.3.5 2.6 1.1 3.8 1.7 2.4 1.2 4.7 2.3 6.6 3.6 3.8 2.4 6 4.5 6 4.5s-3.1 0-7.3-.8c-2.1-.4-4.6-.9-7.2-1.6-1.3-.4-2.7-.7-4-1.1l-2-.7-.5-.2-.2-.1c.2.1-.1 0-.1 0l-.8-.1c-1-.2-2.2-.2-3.3-.2-1.2.1-2.3.2-3.5.4-2.3.5-4.6 1.2-6.6 1.9-4.2 1.5-6.9 2.5-6.9 2.5zM197 169.7s-2.7-1-6.8-2.5c-1-.3-2-.6-3.2-1-1.1-.3-2.3-.6-3.4-.9-1.2-.2-2.3-.4-3.5-.4-1.1 0-2.3 0-3.3.2l-.8.1s-.3.1-.1 0l-.2.1-.5.2-2 .7c-1.4.4-2.7.8-4 1.2-2.6.7-5.1 1.2-7.2 1.6-4.2.8-7.3.8-7.3.8s2.2-2.1 6-4.5c1.9-1.3 4.1-2.4 6.6-3.6 1.2-.6 2.5-1.2 3.8-1.7l2-.8.5-.2.2-.1.5-.1 1.3-.3c3.6-.7 7.1 0 9.9 1.2 2.8 1.2 5.1 2.7 6.8 4.3 3.4 2.9 4.7 5.7 4.7 5.7z'/%3E%3Cpath d='M229.2 220.5c-2.4-13.9-18.8-29-18.6-29.7.1-.2.2-.5.4-.7.8.3 2.6 1.5 3.4 1.5 4.8-.3 7.1-4.7 7.7-6.7 1-3.2 4-19.9 3.7-23.4-.5-7.3-5.1-11.2-9.9-13.2.4-10-.1-21.9-3.1-33.1-1.6-6.2-4.2-12.1-7.4-17.4-8.6-17.5-25.9-32.1-54.4-32.1-15.8 0-28.2 4.5-37.7 11.5-12.2 5.4-22.7 20.7-26.5 38.8-2.4 11.2-2.6 23.1-1.9 32.9-1.8.9-3.5 1.9-4.9 3.4-2.5 2.6-3.8 5.7-3.7 9.4.1 2.8 2.2 19.8 4.2 23.6 1.8 3.4 3.5 7.1 7.2 7.1.8 0 2.8-1.8 3.6-2.1.1.1.1.2.2.4.1.6-15 12.8-17.2 26.9-3.1 19.1-5.7 38.2 3.2 61.3 4.7 12.1 44.7 36.3 73 36.3s66.8-22.8 72.5-34.4c10.1-20.1 9.5-41.6 6.2-60.3zm-13.7-66.4c2.4 1.4 4.6 3.8 4.9 7.8.2 2.4-2.5 18.1-3.5 21.4-.4 1.2-2.2 1.5-3.7 1.5.9-6.1 1.6-23.4 2.3-30.7zM89.1 185.2c-1.7 0-2.9-.8-3.8-2.4-1.2-2.4-3.5-18-3.6-21.1 0-2.2.7-4 2.2-5.5.5-.5 1-.9 1.6-1.3.8 6.6 2.7 23.3 3.9 30.3h-.3zm50.1 37.5c1.5-.2 3-.6 4.2-1.2 8.6-3.8 9.4-4 17.4-.1 1.2.6 2.7 1 4.3 1.3-7.3-.1-18.3-.1-25.9 0zm64.5-27.8c-2.1 2.5-8.1 9.4-13.5 20-1-.4-2.3-.7-3.6-.9-12.2-1.5-11.9-4.3-17.6-7.5-3.3-1.8-9.2-2.2-16.9 0-6.2-2.1-12.9-2.1-16.2-.3-4.3 2.4-9.1 5.9-13.7 7.3-.4-.1-.8 0-1.1.3-1.2.3-2.3.4-3.5.3-2 .3-3.7.9-5.1 1.6-9.8-15.1-14.9-20.3-14.8-20.8 0-.2-.1-.4-.1-.6 0-.1.1-.1.1-.2 2-3.7-1.1-34.5-.8-40.9l1.2-3.2c.9-3.9.8-7.8.2-11.6.4-2 .8-4 1.5-5.9 4-11.3 8.9.5 27.8-18.3 4.4-4.4 6.3-9.3 6.7-14.1 13.8 7.8 30.2 14.8 39.2 14.8 5.6 0 22.4 7.5 27.8 18.1 1.2 2.4 2 4.8 2.5 7.4-.2 3.1-.1 6.3.7 9.7l.3 1.3c.1 7.1-3.3 39.4-1.1 43.5z'/%3E%3C/svg%3E"); --logo-white: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' style='enable-background:new 0 0 380 380' viewBox='0 0 380 380'%3E%3Cpath d='M324.6 186.6c-6.9 0-12.5 5.6-12.5 12.5s5.6 12.5 12.5 12.5 12.5-5.6 12.5-12.5-5.6-12.5-12.5-12.5z' fill='%23fff'/%3E%3Cpath d='M292.6 241h48.3c8.8 0 15.9-7.1 15.9-15.9v-51.9c0-8.8-7.1-15.9-15.9-15.9h-43.3c2.4 10.7 3.7 21.8 3.7 33.2 0 17.7-3 34.7-8.7 50.5z' style='fill:none'/%3E%3Cpath d='M340.9 135.3h-5.3V51.5c0-6.1-4.9-11-11-11s-11 4.9-11 11v83.8h-22.8c2.8 7.1 5.1 14.4 6.8 22h43.3c8.8 0 15.9 7.1 15.9 15.9V225c0 8.8-7.1 15.9-15.9 15.9h-48.3c-2.7 7.6-6 15-9.9 22h58.2c20.9 0 37.9-17 37.9-37.9v-51.8c0-20.9-17-37.9-37.9-37.9z' style='&%2310;' fill='%23fff'/%3E%3Ccircle cx='152.2' cy='190.5' r='150' style='fill:%23ffd400'/%3E%3Cpath d='M101.9 169.6s1.4-2.8 4.8-5.9c1.7-1.5 4-3.1 6.8-4.3 2.8-1.1 6.3-1.8 9.9-1.1l1.3.3.5.1.2.1.5.2 2 .8c1.3.5 2.6 1.1 3.8 1.7 2.4 1.2 4.7 2.3 6.6 3.6 3.8 2.4 6 4.5 6 4.5s-3.1 0-7.3-.8c-2.1-.4-4.6-.9-7.2-1.6-1.3-.4-2.7-.7-4-1.1l-2-.7-.5-.2-.2-.1c.2.1-.1 0-.1 0l-.8-.1c-1-.2-2.2-.2-3.3-.2-1.2.1-2.3.2-3.5.4-2.3.5-4.6 1.2-6.6 1.9-4.2 1.5-6.9 2.5-6.9 2.5zM197 169.7s-2.7-1-6.8-2.5c-1-.3-2-.6-3.2-1-1.1-.3-2.3-.6-3.4-.9-1.2-.2-2.3-.4-3.5-.4-1.1 0-2.3 0-3.3.2l-.8.1s-.3.1-.1 0l-.2.1-.5.2-2 .7c-1.4.4-2.7.8-4 1.2-2.6.7-5.1 1.2-7.2 1.6-4.2.8-7.3.8-7.3.8s2.2-2.1 6-4.5c1.9-1.3 4.1-2.4 6.6-3.6 1.2-.6 2.5-1.2 3.8-1.7l2-.8.5-.2.2-.1.5-.1 1.3-.3c3.6-.7 7.1 0 9.9 1.2 2.8 1.2 5.1 2.7 6.8 4.3 3.4 2.9 4.7 5.7 4.7 5.7z'/%3E%3Cpath d='M229.2 220.5c-2.4-13.9-18.8-29-18.6-29.7.1-.2.2-.5.4-.7.8.3 2.6 1.5 3.4 1.5 4.8-.3 7.1-4.7 7.7-6.7 1-3.2 4-19.9 3.7-23.4-.5-7.3-5.1-11.2-9.9-13.2.4-10-.1-21.9-3.1-33.1-1.6-6.2-4.2-12.1-7.4-17.4-8.6-17.5-25.9-32.1-54.4-32.1-15.8 0-28.2 4.5-37.7 11.5-12.2 5.4-22.7 20.7-26.5 38.8-2.4 11.2-2.6 23.1-1.9 32.9-1.8.9-3.5 1.9-4.9 3.4-2.5 2.6-3.8 5.7-3.7 9.4.1 2.8 2.2 19.8 4.2 23.6 1.8 3.4 3.5 7.1 7.2 7.1.8 0 2.8-1.8 3.6-2.1.1.1.1.2.2.4.1.6-15 12.8-17.2 26.9-3.1 19.1-5.7 38.2 3.2 61.3 4.7 12.1 44.7 36.3 73 36.3s66.8-22.8 72.5-34.4c10.1-20.1 9.5-41.6 6.2-60.3zm-13.7-66.4c2.4 1.4 4.6 3.8 4.9 7.8.2 2.4-2.5 18.1-3.5 21.4-.4 1.2-2.2 1.5-3.7 1.5.9-6.1 1.6-23.4 2.3-30.7zM89.1 185.2c-1.7 0-2.9-.8-3.8-2.4-1.2-2.4-3.5-18-3.6-21.1 0-2.2.7-4 2.2-5.5.5-.5 1-.9 1.6-1.3.8 6.6 2.7 23.3 3.9 30.3h-.3zm50.1 37.5c1.5-.2 3-.6 4.2-1.2 8.6-3.8 9.4-4 17.4-.1 1.2.6 2.7 1 4.3 1.3-7.3-.1-18.3-.1-25.9 0zm64.5-27.8c-2.1 2.5-8.1 9.4-13.5 20-1-.4-2.3-.7-3.6-.9-12.2-1.5-11.9-4.3-17.6-7.5-3.3-1.8-9.2-2.2-16.9 0-6.2-2.1-12.9-2.1-16.2-.3-4.3 2.4-9.1 5.9-13.7 7.3-.4-.1-.8 0-1.1.3-1.2.3-2.3.4-3.5.3-2 .3-3.7.9-5.1 1.6-9.8-15.1-14.9-20.3-14.8-20.8 0-.2-.1-.4-.1-.6 0-.1.1-.1.1-.2 2-3.7-1.1-34.5-.8-40.9l1.2-3.2c.9-3.9.8-7.8.2-11.6.4-2 .8-4 1.5-5.9 4-11.3 8.9.5 27.8-18.3 4.4-4.4 6.3-9.3 6.7-14.1 13.8 7.8 30.2 14.8 39.2 14.8 5.6 0 22.4 7.5 27.8 18.1 1.2 2.4 2 4.8 2.5 7.4-.2 3.1-.1 6.3.7 9.7l.3 1.3c.1 7.1-3.3 39.4-1.1 43.5z'/%3E%3C/svg%3E"); --primary: rgb(15, 23, 42); --dark-primary: #483d8b; --bar-bg: rgb(15, 23, 42); } h1, h2, h3, h4, h5 { box-shadow: none; font-weight: 600; letter-spacing: 0em; --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity)); } h2 { box-shadow: none; margin-left: -2.5rem; margin-right: -2.5rem; border-radius: 0px; --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); padding-left: 2.5rem; padding-right: 2.5rem; padding-top: 1.25rem; padding-bottom: 1.25rem; font-size: 1.25rem; line-height: 1.75rem; --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } h3{ padding-left: 0px; padding-right: 0px; font-size: 1.125rem; line-height: 1.75rem; } h4{ padding-left: 0px; padding-right: 0px; font-size: 1rem; line-height: 1.5rem; } .main-left { min-width: 300px; box-shadow: none; --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); padding-right: 0px; --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } @media screen and (max-width: 768px) { .main-left { min-width: 0; } .main-left.active { min-width: 300px; max-width: 90%; } } .main-left .sidenav-header{ padding-top: 0.75rem; padding-bottom: 0.75rem; } .main-left .sidenav-header .brand, .login-page .login-container .login-form .brand { background: var(--logo); text-indent: -5000px; width: 80px; height: 80px; margin: 0 auto; background-size: 100% auto; } .login-page .login-container .login-form .brand img{ display: none; } header .fill .container .flex1 .brand { background: var(--logo); text-indent: -5000px; width: 48px; height: 48px; margin: 0 auto; background-size: 100% auto; position: absolute; z-index: 51; top: 0.1rem; left: 50%; transform: translateX(-50%); } .main .main-left .nav li.slide .slide-menu{ margin-left: 1rem; margin-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 0.5rem; padding-right: 0.5rem; } .main .main-left .nav li.slide .slide-menu li:not(:first-child){ margin-top: 0.25rem; } .main .main-left .nav li.slide .slide-menu li a{ border-radius: 0.375rem; padding-left: 1rem; padding-right: 1rem; } .main .main-left .nav li.slide .slide-menu li a:hover{ --tw-bg-opacity: 1; background-color: rgb(226 232 240 / var(--tw-bg-opacity)); } .main .main-left .nav li.slide .slide-menu .active::after, .main .main-left .nav li.slide .slide-menu li::after { display: none!important; } .main .main-left .nav li.slide .slide-menu .active a{ --tw-bg-opacity: 1; background-color: rgb(226 232 240 / var(--tw-bg-opacity)); } .main-right>#maincontent>.container{ margin: 0px; --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); padding-left: 2.5rem; padding-right: 2.5rem; } header { /*display: none;*/ } .cbi-section, .cbi-section-error, #iptables, .Firewall form, #cbi-network>.cbi-section-node, #cbi-wireless>.cbi-section-node, #cbi-wireless>#wifi_assoclist_table, [data-tab-title], [data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), [data-page="admin-system-opkg"] #maincontent>.container { box-shadow: none; } #view { overflow: initial; } #view h2 + p, .cbi-map-descr, [data-page="admin-system-attendedsysupgrade-configuration"] .cbi-map .cbi-map-descr, [data-tab-title]>p:first-child, .cbi-section-descr:not(:empty), [data-page="admin-network-firewall-custom"] #view h2 + p, [data-page="admin-status-routes"] #view h2 + p, h3 + .cbi-section-descr, [data-page="admin-system-flash"] .cbi-section-descr{ margin-bottom: 1.25rem; border-radius: 0.375rem; --tw-bg-opacity: 1; background-color: rgb(239 246 255 / var(--tw-bg-opacity)); padding: 1rem; font-size: 0.875rem; line-height: 1.25rem; font-weight: 400; --tw-text-opacity: 1; color: rgb(29 78 216 / var(--tw-text-opacity)); } [data-page="admin-network-firewall-custom"] #view h2 + p, [data-page="admin-status-routes"] #view h2 + p{ margin-top: 1.25rem; } h2 + .cbi-map-descr{ margin-top: 1.25rem; } tr>td, tr>th, .tr>.td, .tr>.th, .cbi-section-table-row::before, #cbi-wireless>#wifi_assoclist_table>.tr:nth-child(2) { border-top: 0; } .table { background: none; min-width: 100%; } .table > :not([hidden]) ~ :not([hidden]){ --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); --tw-divide-opacity: 1; border-color: rgb(229 231 235 / var(--tw-divide-opacity)); } .table{ --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity)); --tw-ring-opacity: 0.05; } @media (min-width: 640px){ .table{ border-radius: 0.5rem; } } .table .tr.table-titles { background: none; border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; } .table .tr:first-child .th:first-child{ border-top-left-radius: 0.5rem; border-width: 0px; --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); padding-top: 0.875rem; padding-bottom: 0.875rem; padding-left: 1rem; padding-right: 0.75rem; text-align: left; font-size: 0.875rem; line-height: 1.25rem; font-weight: 600; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } @media (min-width: 640px){ .table .tr:first-child .th:first-child{ padding-left: 1.5rem; } } .table .td:first-child{ padding-top: 1rem; padding-bottom: 1rem; padding-left: 1rem; padding-right: 0.75rem; text-align: left; font-size: 0.875rem; line-height: 1.25rem; font-weight: 500; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } @media (min-width: 640px){ .table .td:first-child{ padding-left: 1.5rem; } } .table .tr:first-child .th:not(:first-child){ border-width: 0px; --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.875rem; padding-bottom: 0.875rem; text-align: left; font-size: 0.875rem; line-height: 1.25rem; font-weight: 600; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } .table .td:not(:first-child){ padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 0.875rem; line-height: 1.25rem; --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity)); } .table .tr:last-child{ border-bottom-right-radius: 0.5rem; border-bottom-left-radius: 0.5rem; } .table .tr:last-child .td:first-child{ border-bottom-left-radius: 0.5rem; } .table .tr:last-child .td:last-child{ border-bottom-right-radius: 0.5rem; } .cbi-title-section{ margin-left: -2.5rem; margin-right: -2.5rem; margin-bottom: 1.25rem; display: flex; align-items: center; justify-content: space-between; --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); padding-left: 2.5rem; padding-right: 2.5rem; } .cbi-title-section h2{ margin-bottom: 0px; } #indicators{ display: flex; align-items: center; transform: translateY(1rem); z-index: 51; } header.bg-primary .fill{ margin-top: -0.5rem; --tw-bg-opacity: 1; background-color: rgb(226 232 240 / var(--tw-bg-opacity)); padding-top: 0px; padding-bottom: 0.125rem; } header .fill .status span{ display: inline-flex; align-items: center; border-radius: 0.375rem; --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); padding-left: 0.5rem; padding-right: 0.5rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.75rem; line-height: 1rem; font-weight: 500; --tw-text-opacity: 1; color: rgb(75 85 99 / var(--tw-text-opacity)); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-color: rgb(107 114 128 / 0.1); } header .fill .status span[data-style="active"]{ --tw-bg-opacity: 1; background-color: rgb(240 253 244 / var(--tw-bg-opacity)); --tw-text-opacity: 1; color: rgb(21 128 61 / var(--tw-text-opacity)); --tw-ring-color: rgb(22 163 74 / 0.2); } .cbi-section>h3:first-child, .cbi-section>h4:first-child, [data-tab-title]>h3:first-child, [data-tab-title]>h4:first-child{ padding-left: 0px; padding-right: 0px; } header .fill .container .flex1 .showSide { transform: translateY(0.7rem); width: 38px; height: 28px; text-align: center; position: absolute; top: 0; left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; border-top-right-radius: 1rem; border-bottom-right-radius: 1rem; --tw-bg-opacity: 1; background-color: rgb(75 85 99 / var(--tw-bg-opacity)); --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } header .fill .container .flex1 .showSide::before { transform: translateY(0.1rem); text-align: center; font-size: 1.5rem; } .alert, .alert-message{ border-radius: 0.375rem; padding: 1rem; font-weight: 400; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); text-shadow: none; } .alert-message>*{ margin: 0px; } .btn.danger, .alert-message.danger, .alert-message.error { background: none; border-width: 0px; --tw-bg-opacity: 1; background-color: rgb(254 242 242 / var(--tw-bg-opacity)); --tw-text-opacity: 1; color: rgb(185 28 28 / var(--tw-text-opacity)); } #maincontent > .container > #tabmenu{ margin-left: -2.5rem; margin-right: -2.5rem; } #maincontent > .container > #tabmenu > .tabs{ margin-bottom: 0px; border-radius: 0px; padding-left: 0px; padding-right: 0px; } [data-page="admin-system-admin"] .cbi-map h2, [data-page="admin-system-admin-password"] .cbi-map h2{ margin-left: -2.5rem; margin-right: -2.5rem; } [data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), [data-page="admin-system-opkg"] #maincontent>.container{ margin-top: 0px; } footer{ margin-top: 1.25rem; --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); text-align: center; } .tabs, .cbi-tabmenu { background: none; padding: 0; isolation: isolate; margin-left: 0px; margin-right: 0px; margin-bottom: 1.25rem; display: flex; } .tabs > :not([hidden]) ~ :not([hidden]), .cbi-tabmenu > :not([hidden]) ~ :not([hidden]){ --tw-space-x-reverse: 0; margin-right: calc(0.25rem * var(--tw-space-x-reverse)); margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); --tw-divide-x-reverse: 0; border-right-width: calc(1px * var(--tw-divide-x-reverse)); border-left-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); --tw-divide-opacity: 1; border-color: rgb(229 231 235 / var(--tw-divide-opacity)); } .tabs, .cbi-tabmenu{ border-top-width: 1px; border-bottom-width: 1px; --tw-border-opacity: 1; border-bottom-color: rgb(226 232 240 / var(--tw-border-opacity)); padding-top: 0.5rem; padding-bottom: 0.5rem; } .tabs li, .cbi-tabmenu li{ display: flex; align-items: center; padding-top: 0px; padding-bottom: 0px; } .cbi-tabmenu li[class~="cbi-tab"] { border-bottom: 0; } .cbi-tabmenu > li:not(.cbi-tab-disabled), .cbi-tabmenu li[class~="cbi-tab"]:not(.cbi-tab-disabled) .tabs li.active, .tabs li[class~="active"] { background: none; height: auto; border: none; margin: 0; border-radius: 0.375rem; --tw-bg-opacity: 1; padding-top: 0px; padding-bottom: 0px; font-size: 0.875rem; line-height: 1.25rem; font-weight: 500; --tw-text-opacity: 1; color: rgb(67 56 202 / var(--tw-text-opacity)); background-color: rgb(224 231 255 / var(--tw-bg-opacity))!important; } .tabs > li:not(.active), .cbi-tabmenu > .cbi-tab-disabled { background: none; background-color: transparent; border: 0; margin: 0; border-radius: 0.375rem; padding-left: 0.25rem; padding-right: 0.25rem; font-size: 0.875rem; line-height: 1.25rem; font-weight: 500; --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity)); } .tabs > li:not(.active):hover, .cbi-tabmenu > .cbi-tab-disabled:hover{ --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity)); } .tabs > li:not(.active):hover, .cbi-tabmenu > .cbi-tab-disabled:hover, .tabs li:hover { background: none; border: 0; --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); } h2 + .cbi-tabmenu{ margin-left: -2.5rem; margin-right: -2.5rem; padding-left: 2.5rem; padding-right: 2.5rem; } .table + h3{ margin-top: 1.25rem; } body[data-page*="admin-"] #maincontent > .container > #tabmenu > .tabs{ padding-left: 2.5rem; padding-right: 2.5rem; } body[data-page*="admin-status-"] #maincontent > .container > #tabmenu > .tabs{ margin-bottom: 1.25rem; } #content_syslog { box-shadow: none; } #syslog{ margin-top: 1.25rem; --tw-bg-opacity: 1; background-color: rgb(15 23 42 / var(--tw-bg-opacity)); padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); } .login-page .login-container .login-form .cbi-button-apply{ letter-spacing: 0em; } .login-page .login-container footer, .login-page .main-bg{ display: none; } .login-page .login-container{ margin-top: 0px; margin-bottom: 0px; margin-left: auto; margin-right: auto; display: flex; width: 100%; align-items: center; justify-content: center; padding: 0px; } .login-page .login-container .login-form{ margin-left: auto; margin-right: auto; margin-top: 1.25rem; border-radius: 0.375rem; --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); padding-top: 2.5rem; padding-bottom: 2.5rem; --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); min-height: 0; } .login-page .login-container .login-form .cbi-button-apply{ margin-top: 0px; margin-bottom: 0px; } .login-page .login-container .login-form .form-login .input-group input{ display: block; height: auto; width: 100%; border-radius: 0.375rem; border-width: 0px; padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; font-size: 1rem; line-height: 1.5rem; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); } .login-page .login-container .login-form .form-login .input-group input::placeholder{ --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); } .login-page .login-container .login-form .form-login .input-group input:focus{ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity)); } .login-page .login-container .login-form .form-login .input-group input { padding-left: calc(24px + 1rem); } .login-page .login-container .login-form .form-login .input-group input:focus{ border-width: 0px; --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .login-page .login-container .login-form .form-login .input-group .border { display: none!important; } .cbi-input-textarea, textarea, [data-page="admin-system-crontab"] #view p textarea, [data-page="admin-system-startup"] textarea{ display: block; height: auto; width: 100%; border-radius: 0.375rem; border-width: 0px; padding-left: 1rem; padding-right: 1rem; padding-top: 1rem; padding-bottom: 1rem; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0em; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); } .cbi-input-textarea::placeholder, textarea::placeholder, [data-page="admin-system-crontab"] #view p textarea::placeholder, [data-page="admin-system-startup"] textarea::placeholder{ --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); } .cbi-input-textarea:focus, textarea:focus, [data-page="admin-system-crontab"] #view p textarea:focus, [data-page="admin-system-startup"] textarea:focus{ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity)); } .cbi-input-text, #localtime, .cbi-input-select, .cbi-dropdown>ul>li input[type="text"]{ display: block; height: auto; width: 100%; border-radius: 0.375rem; border-width: 0px; padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; font-size: 1rem; line-height: 1.5rem; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); } .cbi-input-text::placeholder, #localtime::placeholder, .cbi-input-select::placeholder, .cbi-dropdown>ul>li input[type="text"]::placeholder{ --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); } .cbi-input-text:focus, #localtime:focus, .cbi-input-select:focus, .cbi-dropdown>ul>li input[type="text"]:focus{ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity)); } .control-group input{ padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; } select:not([multiple="multiple"]):focus, input:not(.cbi-button):focus, .cbi-dropdown:focus, #localtime:focus, .cbi-input-select:focus{ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-opacity: 1; --tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity)); } .cbi-value-description{ padding-left: 0px; padding-right: 0px; font-size: 0.875rem; line-height: 1.25rem; --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); } #view h2 + p{ margin-top: 1.25rem; } .cbi-section-create{ padding-left: 0px; padding-right: 0px; } @media screen and (min-width: 1px) { .btn:only-child, .cbi-button:only-child { margin-left: 0!important; } } .cbi-value-field br { display: none; } .cbi-map:not(:first-child){ margin-top: 0px; } .table .cbi-value-field br { display: block; } .btn, .cbi-button, .item::after{ padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; font-size: 1rem; line-height: 1.5rem; } .login-page .login-container .login-form .form-login .input-group::before { font-size: 1.2rem; } .Dashboard .internet-status-self .internet-status-info .title, .Dashboard .title { height: initial; display: flex; align-items: center; justify-content: space-between; } .Dashboard .title img { height: 24px; width: auto!important; } .main-right>#maincontent .Dashboard h3{ text-align: right; } .main-right>#maincontent .Dashboard .settings-info .label{ padding-top: 0px; padding-bottom: 0px; padding-left: 0.5rem; padding-right: 0.5rem; font-size: 0.875rem; line-height: 1.25rem; font-weight: 400; } .Dashboard .internet-status-self .settings-info p:nth-child(2) span:first-child, .Dashboard .router-status-wifi .wifi-info .settings-info p:first-child span:first-child, .Dashboard .router-status-wifi .wifi-info .settings-info p:nth-child(2) span:first-child{ font-size: 1rem; line-height: 1.5rem; } .Dashboard .settings-info p span:first-child{ line-height: 1.5rem; font-weight: 600; font-size: 1rem!important; } @media screen and (min-width: 0px) { .label.label-success{ --tw-bg-opacity: 1; background-color: rgb(240 253 244 / var(--tw-bg-opacity)); --tw-text-opacity: 1; --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-inset: inset; --tw-ring-color: rgb(22 163 74 / 0.2); color: rgb(21 128 61 / var(--tw-text-opacity))!important; } } .Dashboard .router-status-self .router-status-info .settings-info{ padding-left: 0px; } .Dashboard .section-content .internet-status-info .settings-info, .Dashboard .router-status-wifi .wifi-info .settings-info, .Dashboard .router-status-lan .lan-info .settings-info{ justify-content: space-between; } .cbi-progressbar{ --tw-bg-opacity: 1; background-color: rgb(148 163 184 / var(--tw-bg-opacity)); height: 1.5rem; } .cbi-progressbar>div{ --tw-bg-opacity: 1; background-color: rgb(15 23 42 / var(--tw-bg-opacity)); } .cbi-progressbar::after{ padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.75rem; line-height: 1rem; --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); } .table .td:not(:first-child), .table .td:first-child{ font-size: 1rem; line-height: 1.5rem; } .table .td:first-child, .table .td:not(:first-child), .table .tr:first-child .th:not(:first-child), .table .tr:first-child .th:first-child{ padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 1rem; } .td, .th{ text-align: left; } .nft-chain-hook{ margin-bottom: 1.25rem; border-radius: 0.375rem; --tw-bg-opacity: 1; background-color: rgb(239 246 255 / var(--tw-bg-opacity)); padding: 1rem; font-size: 0.875rem; line-height: 1.25rem; font-weight: 400; --tw-text-opacity: 1; color: rgb(29 78 216 / var(--tw-text-opacity)); } .nft-rules{ margin-bottom: 1.25rem; } .nft-table > h3, [data-page="admin-system-opkg"] h2{ margin-left: -2.5rem; margin-right: -2.5rem; border-radius: 0px; border-bottom-width: 1px; --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); padding-left: 2.5rem; padding-right: 2.5rem; padding-top: 1.25rem; padding-bottom: 1.25rem; font-size: 1.5rem; line-height: 2rem; --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } [data-page="admin-system-opkg"] .cbi-tabmenu{ margin-left: -2.5rem; margin-right: -2.5rem; padding-left: 2.5rem; padding-right: 2.5rem; } label, .label-status{ display: inline-flex; align-items: center; font-size: 1rem; line-height: 1.5rem; } .label-status{ display: inline-flex; align-items: center; border-radius: 0.375rem; padding-left: 0.5rem; padding-right: 0.5rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.75rem; line-height: 1rem; font-weight: 500; } label>input[type="checkbox"], label>input[type="radio"] { top: initial; right: initial; margin-right: 0.5rem; font-size: 1rem; line-height: 1.5rem; } @media screen and (min-width: 0px) { body[data-page="admin-system-opkg"] .controls { margin-top: 0!important; margin-left: -2.5rem!important; margin-right: -2.5rem!important; margin-bottom: 1.25rem; padding-left: 2.5rem; padding-right: 2.5rem; } body[data-page="admin-system-opkg"] h2 + .controls{ --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); padding-top: 1.25rem; padding-bottom: 1.25rem; --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); margin-bottom: 0!important; } body[data-page="admin-system-opkg"] h2 + .controls > div:first-child{ margin-bottom: 1.25rem; } #maincontent > .alert-message:first-child{ margin: 0px; align-items: center; padding-left: 2.5rem; padding-right: 2.5rem; border-radius: 0!important; } #maincontent > .alert-message:first-child > div:last-child{ text-align: right; flex: initial !important; } .cbi-dropdown[open]>ul.dropdown { max-height: 210px!important; } } .controls>*>.btn:not([aria-label$="page"]){ margin: 0px; --tw-bg-opacity: 1; background-color: rgb(15 23 42 / var(--tw-bg-opacity)); padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; } .cbi-dropdown.cbi-button-apply { padding-top: 0.594rem; padding-bottom: 0.594rem; } .cbi-page-actions{ padding-left: 0px; padding-right: 0px; } body[data-page="admin-services-ruantiblock-service"] h2{ margin-bottom: 1.25rem; } .cbi-dropdown>ul>li input[type="text"] { height: initial; } .cbi-value input[type="password"]{ font-size: 1rem; line-height: 1.5rem; } h2 + .cbi-section > .cbi-tabmenu:first-child{ margin-left: -2.5rem; margin-right: -2.5rem; padding-left: 2.5rem; padding-right: 2.5rem; } ::selection{ --tw-bg-opacity: 1; background-color: rgb(199 210 254 / var(--tw-bg-opacity)); --tw-text-opacity: 1; color: rgb(0 0 0 / var(--tw-text-opacity)); } .main .main-left .nav>li>a[data-title="Wizard"]:first-child.active::before { background: var(--logo-white); background-size: 100% auto; content: ''; width: 19px; height: 19px; } .main .main-left .nav>li:not(.active)>a[data-title="Wizard"]:first-child::before { background: var(--logo); background-size: 100% auto; content: ''; width: 19px; height: 19px; } .network-status-table{ margin-top: 1.25rem; margin-bottom: 1.25rem; display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 1.25rem; } @media (min-width: 640px){ .network-status-table{ grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (min-width: 1024px){ .network-status-table{ grid-template-columns: repeat(3, minmax(0, 1fr)); } } .ifacebox, .network-status-table .ifacebox{ position: relative; margin: 0px; border-radius: 0.5rem; --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); padding-top: 0px; padding-bottom: 0px; padding-left: 0px; padding-right: 0px; --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); overflow: initial; } .ifacebox-head, .ifacebox-head.active, .network-status-table .ifacebox-head, .network-status-table .ifacebox-head.active { background: none; border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; --tw-bg-opacity: 1; background-color: rgb(229 231 235 / var(--tw-bg-opacity)); padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; text-align: right; font-size: 1rem; line-height: 1.5rem; font-weight: 400; --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } .ifacebox-head.cbi-tooltip-container { border-radius: 0!important; } .ifacebox-head.active, .network-status-table .ifacebox-head.active{ --tw-bg-opacity: 1; background-color: rgb(187 247 208 / var(--tw-bg-opacity)); } .ifacebox-head.active *, .network-status-table .ifacebox-head.active *{ --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } .ifacebox-body, .network-status-table .ifacebox-body{ padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 1rem; } .network-status-table .ifacebox-body>span { } .ifacebox-body .ifacebadge, .network-status-table .ifacebox-body .ifacebadge:not(.cbi-tooltip) { background-color: transparent; margin-top: 0.5rem; align-items: flex-start; border-top-width: 1px; --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity)); padding: 0px; padding-top: 1rem; } .network-status-table .ifacebox-body .ifacebadge>span { } .ifacebox-body .ifacebadge img, .network-status-table .ifacebox-body .ifacebadge:not(.cbi-tooltip) img { position: absolute; left: 10px; top: 0.75rem; margin: 0px; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox-body img { position: absolute; left: 10px; top: 0.5rem; margin: 0px; } div[style*="display:grid;grid-template-columns:repeat"] img+br{ display: none; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox-body, div[style*="display:grid;grid-template-columns:repeat"] .ifacebox-body span { font-size: 1rem!important; } .network-status-table .nowrap:not(.td){ text-overflow: ellipsis; max-width: 100%; overflow: hidden; display: block; } .network-status-table .nowrap:not(.td) + br { display: none; } div[style*="display:grid;grid-template-columns:repeat"]{ margin-top: 1.25rem; margin-bottom: 1.25rem; display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 1.25rem; } @media (min-width: 640px){ div[style*="display:grid;grid-template-columns:repeat"]{ grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (min-width: 1024px){ div[style*="display:grid;grid-template-columns:repeat"]{ grid-template-columns: repeat(4, minmax(0, 1fr)); } } div[style*="display:grid;grid-template-columns:repeat"] { grid-template-columns: repeat(4, minmax(0, 1fr))!important; } div[style*="display:grid;grid-template-columns:repeat"] .ifacebox{ position: relative; margin: 0px; border-radius: 0.5rem; --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); padding-top: 0px; padding-bottom: 0px; padding-left: 0px; padding-right: 0px; --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); max-width: none!important; flex-basis: initial!important; min-width: initial!important; margin: 0!important; width: calc(25% - 1.25rem); overflow: initial; } #cbi-network-interface .table, #cbi-network-interface .cbi-section-table-row, #cbi-network-interface div>table>tbody>tr:nth-of-type(2n), div>.table>.tr:nth-of-type(2n){ --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); box-shadow: none; } #cbi-network-interface .table>.tr>.td.cbi-value-field{ padding-left: 0px; padding-right: 0px; padding-top: 1.25rem; padding-bottom: 1.25rem; text-align: left!important; vertical-align: top!important; } #cbi-network-interface .ifacebox { min-width: 180px; } #lan-ifc-devices > span:nth-child(2){ display: flex; flex-direction: row; align-items: center; gap: 0.5rem; } .cbi-tooltip-container:hover .cbi-tooltip.ifacebadge{ --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); padding: 0.5rem; } .cbi-tooltip-container:hover .cbi-tooltip.ifacebadge img { left: initial; right: 10px; } #cbi-network-interface .ifacebox-body > .cbi-tooltip-container:first-child > img { position: absolute; left: 10px; top: 0.75rem; margin: 0px; } #cbi-network-interface .ifacebox-body > br{ display: none; } #cbi-network-interface .ifacebox-body small{ font-size: 1rem; line-height: 1.5rem; } #cbi-network-interface .table>.tr>.td.cbi-value-field strong{ --tw-text-opacity: 1; color: rgb(0 0 0 / var(--tw-text-opacity)); } #cbi-network-interface .table .td:not(:first-child){ padding-left: 0px; padding-right: 0px; padding-top: 1.25rem; padding-bottom: 1.25rem; vertical-align: top!important; } ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/routerich/icon/browserconfig.xml ================================================ #ffffff ================================================ FILE: luci/themes/luci-theme-routerich/htdocs/luci-static/routerich/icon/manifest.json ================================================ { "name": "Openwrt", "icons": [ { "src": "\/android-icon-36x36.png", "sizes": "36x36", "type": "image\/png", "density": "0.75" }, { "src": "\/android-icon-48x48.png", "sizes": "48x48", "type": "image\/png", "density": "1.0" }, { "src": "\/android-icon-72x72.png", "sizes": "72x72", "type": "image\/png", "density": "1.5" }, { "src": "\/android-icon-96x96.png", "sizes": "96x96", "type": "image\/png", "density": "2.0" }, { "src": "\/android-icon-144x144.png", "sizes": "144x144", "type": "image\/png", "density": "3.0" }, { "src": "\/android-icon-192x192.png", "sizes": "192x192", "type": "image\/png", "density": "4.0" } ] } ================================================ FILE: luci/themes/luci-theme-routerich/luasrc/view/themes/routerich/footer.htm ================================================ <%# Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template luci-theme-argon Copyright 2020 Jerrykuku Have a bug? Please create an issue here on GitHub! https://github.com/jerrykuku/luci-theme-argon/issues luci-theme-material: Copyright 2015 Lutty Yang Agron Theme https://demos.creative-tim.com/argon-dashboard/index.html Licensed to the public under the Apache License 2.0 -%> <% local ver = require "luci.version" %>
================================================ FILE: luci/themes/luci-theme-routerich/luasrc/view/themes/routerich/footer_login.htm ================================================ <%# Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template luci-theme-argon Copyright 2020 Jerrykuku Have a bug? Please create an issue here on GitHub! https://github.com/jerrykuku/luci-theme-argon/issues luci-theme-material: Copyright 2015 Lutty Yang Agron Theme https://demos.creative-tim.com/argon-dashboard/index.html Licensed to the public under the Apache License 2.0 -%> <% local ver = require "luci.version" %> ================================================ FILE: luci/themes/luci-theme-routerich/luasrc/view/themes/routerich/header.htm ================================================ <%# Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template luci-theme-argon Copyright 2020 Jerrykuku Have a bug? Please create an issue here on GitHub! https://github.com/jerrykuku/luci-theme-argon/issues luci-theme-material: Copyright 2015 Lutty Yang Argon Theme https://demos.creative-tim.com/argon-dashboard/index.html Licensed to the public under the Apache License 2.0 -%> <% local sys = require "luci.sys" local util = require "luci.util" local http = require "luci.http" local disp = require "luci.dispatcher" local ver = require "luci.version" local boardinfo = util.ubus("system", "board") local node = disp.context.dispatched local fs = require "nixio.fs" local nutil = require "nixio.util" local uci = require 'luci.model.uci'.cursor() -- send as HTML5 http.prepare_content("text/html") math.randomseed(os.time()) -- Custom settings local mode = 'normal' local dark_css = fs.readfile('/www/luci-static/routerich/css/dark.css') local bar_color = '#5e72e4' local primary, dark_primary, blur_radius, blur_radius_dark, blur_opacity if fs.access('/etc/config/routerich') then primary = uci:get_first('routerich', 'global', 'primary') dark_primary = uci:get_first('routerich', 'global', 'dark_primary') blur_radius = uci:get_first('routerich', 'global', 'blur') blur_radius_dark = uci:get_first('routerich', 'global', 'blur_dark') blur_opacity = uci:get_first('routerich', 'global', 'transparency') blur_opacity_dark = uci:get_first('routerich', 'global', 'transparency_dark') mode = uci:get_first('routerich', 'global', 'mode') bar_color = mode == 'dark' and dark_primary or primary end -- Brand name local brand_name = boardinfo.hostname or "?" -%> <%=striptags( (boardinfo.hostname or "?") .. ( (node and node.title) and ' - ' .. translate(node.title) or '')) %> - LuCI - LuCI"> - LuCI"> <% if node and node.css then %> <% end -%> <% if css then %> <% end -%> ">
<%- if luci.sys.process.info("uid") == 0 and luci.sys.user.getuser("root") and not luci.sys.user.getpasswd("root") then -%>

<%:No password set!%>

<%:There is no password set on this router. Please configure a root password to protect the web interface.%>

<% if disp.lookup("admin/system/admin") then %> <% end %>
<%- end -%> ================================================ FILE: luci/themes/luci-theme-routerich/luasrc/view/themes/routerich/header_login.htm ================================================ <%# Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template luci-theme-argon Copyright 2020 Jerrykuku Have a bug? Please create an issue here on GitHub! https://github.com/jerrykuku/luci-theme-argon/issues luci-theme-material: Copyright 2015 Lutty Yang Argon Theme https://demos.creative-tim.com/argon-dashboard/index.html Licensed to the public under the Apache License 2.0 -%> <% local sys = require "luci.sys" local util = require "luci.util" local http = require "luci.http" local disp = require "luci.dispatcher" local ver = require "luci.version" local boardinfo = util.ubus("system", "board") local node = disp.context.dispatched local fs = require "nixio.fs" local nutil = require "nixio.util" local uci = require 'luci.model.uci'.cursor() -- send as HTML5 http.prepare_content("text/html") math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 9))) -- Custom settings local mode = 'normal' local dark_css = fs.readfile('/www/luci-static/routerich/css/dark.css') local bar_color = '#5e72e4' local primary, dark_primary, blur_radius, blur_radius_dark, blur_opacity if fs.access('/etc/config/routerich') then primary = uci:get_first('routerich', 'global', 'primary') dark_primary = uci:get_first('routerich', 'global', 'dark_primary') blur_radius = uci:get_first('routerich', 'global', 'blur') blur_radius_dark = uci:get_first('routerich', 'global', 'blur_dark') blur_opacity = uci:get_first('routerich', 'global', 'transparency') blur_opacity_dark = uci:get_first('routerich', 'global', 'transparency_dark') mode = uci:get_first('routerich', 'global', 'mode') bar_color = mode == 'dark' and dark_primary or primary end -%> <%=striptags( (boardinfo.hostname or "?") .. ( (node and node.title) and ' - ' .. translate(node.title) or '')) %> - LuCI - LuCI"> - LuCI"> <% if node and node.css then %> <% end -%> <% if css then %> <% end -%> ================================================ FILE: luci/themes/luci-theme-routerich/luasrc/view/themes/routerich/out_header_login.htm ================================================ <%# Copyright 2008 Steven Barth Copyright 2008-2019 Jo-Philipp Wich Licensed to the public under the Apache License 2.0. -%> <% local ver = require "luci.version" if not luci.dispatcher.context.template_header_sent then include("themes/" .. theme .. "/header_login") luci.dispatcher.context.template_header_sent = true end %> ================================================ FILE: luci/themes/luci-theme-routerich/luasrc/view/themes/routerich/sysauth.htm ================================================ <%# Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI and Argon Template luci-theme-argon Copyright 2020 Jerryk Have a bug? Please create an issue here on GitHub! https://github.com/jerrykuku/luci-theme-argon/issues luci-theme-bootstrap: Copyright 2008 Steven Barth Copyright 2008-2016 Jo-Philipp Wich Copyright 2012 David Menting MUI: https://github.com/muicss/mui Argon Theme https://demos.creative-tim.com/argon-dashboard/index.html Licensed to the public under the Apache License 2.0 -%> <%+themes/routerich/out_header_login%> <% local util = require "luci.util" local fs = require "nixio.fs" local nutil = require "nixio.util" local json = require "luci.jsonc" local sys = require "luci.sys" local uci = require 'luci.model.uci'.cursor() -- Fetch Local Background Media local function glob(...) local iter, code, msg = fs.glob(...) if iter then return nutil.consume(iter) else return nil, code, msg end end local imageTypes = " jpg png gif webp " local videoTypes = " mp4 webm " local allTypes = imageTypes .. videoTypes local function fetchMedia(path, themeDir) local backgroundTable = {} local backgroundCount = 0 for i, f in ipairs(glob(path)) do attr = fs.stat(f) if attr then local ext = fs.basename(f):match(".+%.(%w+)$") if ext ~= nil then ext = ext:lower() end if ext ~= nil and string.match(allTypes, " "..ext.." ") ~= nil then local bg = {} bg.type = ext bg.url = themeDir .. fs.basename(f) table.insert(backgroundTable, bg) backgroundCount = backgroundCount + 1 end end end return backgroundTable, backgroundCount end local function selectBackground(themeDir) local bgUrl = media .. "/img/bg1.jpg" local backgroundType = "Image" local mimeType = "" if fs.access("/etc/config/routerich") then local online_wallpaper = uci:get_first('routerich', 'global', 'online_wallpaper') or (uci:get_first('routerich', 'global', 'bing_background') == '1' and 'bing') if (online_wallpaper and online_wallpaper ~= "none") then local picurl = sys.exec("/usr/libexec/routerich/online_wallpaper") if (picurl and picurl ~= '') then return picurl, "Image", "" end end end local backgroundTable, backgroundCount = fetchMedia("/www" .. themeDir .. "*", themeDir) if ( backgroundCount > 0 ) then local currentBg = backgroundTable[math.random(1, backgroundCount)] bgUrl = currentBg.url if (string.match(videoTypes, " "..currentBg.type.." ") ~= nil) then backgroundType = "Video" mimeType = "video/" .. currentBg.type end end return bgUrl, backgroundType, mimeType end local boardinfo = util.ubus("system", "board") local themeDir = media .. "/background/" local bgUrl, backgroundType, mimeType = selectBackground(themeDir) %>