1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
#
# Copyright (C) 2026 Ivan Kvashonkin
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-footstrap
# luci.mk keys the Build/Prepare hook name on LUCI_NAME, which defaults to the checkout's
# directory name — a differently-named checkout symlinked into a feed would silently skip the
# hook below. Pin it.
LUCI_NAME:=luci-theme-footstrap
PKG_MAINTAINER:=Ivan Kvashonkin <vizzlef@gmail.com>
LUCI_TITLE:=Footstrap Theme
LUCI_DESCRIPTION:=A standalone LuCI theme with a collapsible sidebar or top bar, light/dark \
modes, a menu search and a client-side page router
# +luci-base is the WHOLE list, and keeping it that way is a design constraint: the theme ships no
# framework and every page it draws is drawn by luci-base's own view JS.
LUCI_DEPENDS:=+luci-base
LUCI_PKGARCH:=all
# csstidy is old enough to mangle :has() and color-mix(), both of which this sheet uses heavily —
# it silently drops the declarations and the layout goes with them. The stylesheet is already
# minified (see the note below), so there is nothing for it to win back.
LUCI_MINIFY_CSS:=0
#
# The JS keeps luci.mk's default jsmin. The catch, and why the source is written the way it is:
# jsmin decides `/` = regex-or-division from a ONE-character lookback, and `n` (of `return`) and
# `>` (of `=>`) are not on its allow-list — so `return /re/` makes it eat the rest of the file AND
# EXIT 0 (openwrt/luci#8299). Every regex literal in this theme's JS is therefore parenthesised.
# Apache-2.0, and not a free choice: the stylesheet began as a fork of luci-theme-bootstrap's
# cascade.css, the ucode templates derive from LuCI's own, and a few JS helpers are verbatim
# copies. The notices travel with it.
#
# The theme carries NO webfonts: --fs-font-sans and --fs-font-mono name Manrope and JetBrains Mono
# first and the system stack after, so a machine with either installed uses it and one without
# falls through silently. Nothing here is Font Software, so there is no OFL half to declare.
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
# /etc/config/footstrap is SHIPPED as an empty stub and WRITTEN AT RUNTIME: Appearance -> "Save as
# default" has rpcd uci-set the router-wide axes into that very file (fs-prefs.js saveAsDefault()).
# Without this define the package manager owns it as an ordinary file and REPLACES it on upgrade,
# so an admin's saved defaults are wiped by an ordinary package upgrade, silently, reported as
# success. OpenWrt honours this for BOTH formats (include/package-pack.mk: KEEP_$(1) -> apk
# .conffiles, ipk CONTROL/conffiles). Any future root/etc/config/* must be listed here too.
define Package/luci-theme-footstrap/conffiles
/etc/config/footstrap
endef
# NO `rpcd reload` HERE, on install or on removal. Measured on a live router: `reload` is
# procd_send_signal -> SIGHUP -> exec_self(), a full re-exec that re-scans the plugin dirs and
# dlopen()s every .so, and a plugin that is absent or mid-write at that instant is DROPPED past
# one stderr line and does not come back on its own. Losing `file` that way makes System ->
# Backup / Flash Firmware's "Reset to defaults" row silently disappear, and the same race was
# seen on the `luci` plugin, leaving getFeatures/getTimezones answering -32000 until a reboot.
# This package registers no rpcd object of its own - only acl.d/*.json - so the reload ran that
# risk for every OTHER plugin and refreshed nothing of ours.
#
# rpcd reads acl.d/*.json AT LOGIN, not only on SIGHUP, so a fresh login already sees a
# just-installed ACL. The one case that leaves is a session opened BEFORE this install on a
# restricted (non-'*') rpcd login: its in-memory ACL set predates the grant. That session is
# ended below, and only when re-authenticating would actually hand it the theme's scope.
define Package/luci-theme-footstrap/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
# uci-defaults registers the theme; drop the LuCI caches so it is seen without a reboot.
# Calling it ourselves is belt-and-braces: OpenWrt's default_postinst also runs (then
# deletes) every /etc/uci-defaults/* we ship, so it executes twice per install. It is
# idempotent: the second pass finds the theme already registered and changes nothing.
[ -f /etc/uci-defaults/30_luci-theme-footstrap ] && \
sh /etc/uci-defaults/30_luci-theme-footstrap >/dev/null 2>&1 || true
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
# A blanket ('*') login is granted everything and answers true below, so it is never touched;
# neither is the unauthenticated all-zero session, nor a denied session whose own login does
# not list this theme's group - re-authenticating would deny it just the same. Guarded on
# rpcd having ubus session support at all.
ubus list 2>/dev/null | grep -qx session && {
for s in $$(ubus call session list 2>/dev/null | grep -o '"ubus_rpc_session": "[a-f0-9]*"' | cut -d'"' -f4); do
case "$$s" in *[!0]*) ;; *) continue ;; esac
ubus call session access "{\"ubus_rpc_session\":\"$$s\",\"scope\":\"uci\",\"object\":\"footstrap\",\"function\":\"write\"}" 2>/dev/null | grep -q true && continue
u=$$(ubus call session get "{\"ubus_rpc_session\":\"$$s\"}" 2>/dev/null | grep -o '"username": *"[^"]*"' | cut -d'"' -f4)
[ -n "$$u" ] || continue
# Every login section from `uci show`, not a `uci -q get` index walk: the latter stops
# at the first section with no username option. `while read` keeps a username with a
# space in one piece; the loop is a subshell, so its answer is echoed out, not stored.
grant=$$(uci show rpcd 2>/dev/null | grep -F .username= | while read -r kv; do
sect=$${kv%%.username=*}
un=$${kv#*.username=}
un=$${un#\'}
un=$${un%\'}
[ "$$un" = "$$u" ] || continue
for grp in $$(uci -q get "$$sect.read") $$(uci -q get "$$sect.write"); do
[ "$$grp" = luci-theme-footstrap ] && { echo 1; break 2; }
done
done)
[ -n "$$grant" ] || continue
ubus call session destroy "{\"ubus_rpc_session\":\"$$s\"}" >/dev/null 2>&1 || true
done
}
}
exit 0
endef
define Package/luci-theme-footstrap/postrm
#!/bin/sh
# opkg runs the OLD package's postrm with arg "upgrade" during a version upgrade and "remove" on a
# real removal. On upgrade this script MUST change nothing: reverting mediaurlbase and wiping the
# theme registration here is what flipped every updating user back to bootstrap. apk never runs
# this on upgrade (it uses the new package's pre/post-upgrade), so guarding on the arg is correct
# for both managers.
case "$$1" in *upgrade*) exit 0 ;; esac
[ -n "$${IPKG_INSTROOT}" ] || {
uci -q delete luci.themes.Footstrap
# Don't leave the active theme pointing at the media dir we just removed. A theme needs its
# media dir AND its ucode template to render, so a one-part check could hand the UI to a
# half-removed bootstrap — the blank page this branch exists to avoid.
case "$$(uci -q get luci.main.mediaurlbase)" in
/luci-static/footstrap*)
[ -d /www/luci-static/bootstrap ] && \
[ -f /usr/share/ucode/luci/template/themes/bootstrap/header.ut ] && \
uci set luci.main.mediaurlbase=/luci-static/bootstrap ;;
esac
uci commit luci
# The admin-uploaded login background and pattern, kept out of the package on purpose so they
# survive an upgrade. A real removal is the one time they should go.
rm -rf /etc/footstrap >/dev/null 2>&1 || true
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
}
endef
# THE STYLESHEET IS COMMITTED, NOT GENERATED HERE, and that is a deliberate difference from the
# theme's own repository. There, styles/ is sixteen files in four cascade layers whose ORDER is the
# design, and cascade.css is a build artefact that is not even tracked; a shell script concatenates
# and minifies it at package time. In this tree the other four themes each commit one cascade.css
# and have no build step, and a theme that arrives with its own build system asks a reviewer to
# audit that before they can read a stylesheet. So the sheet is generated upstream of this tree and
# committed here — unmangled and readable, which the release artefact is not.
#
# What is left for this hook is the two things luci.mk cannot do by itself.
define Build/Prepare/luci-theme-footstrap
# luci.mk's Build/Prepare copies only src/ luasrc/ htdocs/ root/ ucode/ into
# $(PKG_BUILD_DIR), and PKG_LICENSE_FILES resolves against THAT — so the Apache text has to be
# put there by hand or the declaration points at nothing.
$(CP) $(CURDIR)/LICENSE $(PKG_BUILD_DIR)/
# The version the Appearance tab prints. luci.mk derives PKG_SRC_VERSION from the tree's git
# history; without this the string stays at the source literal '0.0.0-dev' and every install
# reports itself as a development build.
$(SED) "s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$(if $(PKG_VERSION),$(PKG_VERSION),$(PKG_SRC_VERSION))'#" \
$(PKG_BUILD_DIR)/htdocs/luci-static/resources/fs-version.js
endef
include ../../luci.mk
# THE LINE BELOW IS LOAD-BEARING AND IT IS NOT A SIGNATURE — do not delete it as boilerplate.
#
# include/scan.mk builds the package list by GREPPING the Makefiles, not by parsing them:
# find -L package -name Makefile | xargs grep -aHE 'call (Build/DefaultTargets|BuildPackage|KernelPackage)'
# A Makefile that does not match is not in the list, so it is never dumped, no CONFIG_PACKAGE_*
# symbol is emitted for it, and `make package/<name>/compile` answers "No rule to make target" —
# with no error naming the package anywhere in the build.
#
# This theme never calls BuildPackage itself; luci.mk does, at the include above. The grep cannot
# see that. So the only thing that puts this package into the build's list is the literal text on
# the next line — which is why every luci-* Makefile carries it.
# call BuildPackage - OpenWrt buildroot signature
|