mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 21:22:52 +08:00
213 lines
10 KiB
Markdown
213 lines
10 KiB
Markdown
# Implementation notes
|
|
|
|
Load-bearing facts about LuCI and OpenWrt that this package depends on, why the
|
|
code is shaped the way it is, and how to re-verify each claim. None of it is
|
|
derivable from this repository alone: the upstream behaviour lives in
|
|
`luci-base`, `luci-mod-status` and the active theme.
|
|
|
|
Upstream references are paths inside `openwrt/luci`.
|
|
|
|
## The Overview page rebuilds itself every poll cycle
|
|
|
|
`luci-mod-status/htdocs/luci-static/resources/view/status/index.js` builds one
|
|
`.cbi-section` per include **once**, in `render()`, and then on every poll cycle
|
|
calls each include's `load()` and `render()` again and replaces the section
|
|
*content* through `dom.content`. The interval is `luci.main.pollinterval`,
|
|
default 5 seconds.
|
|
|
|
Two consequences drive the design of `luci/overview.js`:
|
|
|
|
1. **Never move a section unless the order actually changed.** Re-appending a
|
|
node re-inserts it, and re-insertion restarts its CSS animations. The theme
|
|
puts `fade-in` on every revealed section — `.fade-in { animation: fade-in
|
|
.4s ease }` with `opacity` `0 → 1` in
|
|
`themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css` — so
|
|
re-appending all sections on each cycle makes the whole page flash every
|
|
five seconds. `applyLayout()` therefore compares the wanted order against
|
|
the live DOM order and touches nothing when they match.
|
|
|
|
2. **Never clear a visibility class and re-add it.** Dropping
|
|
`overview-manager-hidden` from every section and then re-adding it exposes
|
|
every hidden widget for a frame. Use `classList.toggle(name, force)`, which
|
|
only mutates when the state really changes.
|
|
|
|
`scripts/test-overview-ui.js` pins both: it counts `appendChild` calls and class
|
|
mutations across a second identical render and fails if either is non-zero.
|
|
Reverting either property makes it fail, which is worth re-checking if the test
|
|
is ever refactored.
|
|
|
|
`dom.content` (in `luci.js`) removes all children and appends the new ones in
|
|
one synchronous task, so replacing our own container each cycle cannot paint an
|
|
intermediate state. Only node *moves* and class churn are visible.
|
|
|
|
`load()` caches its result for the lifetime of the page. The saved layout cannot
|
|
change while the Overview page stays open, and the alternative is two helper
|
|
process spawns every five seconds forever. A failed read is deliberately not
|
|
cached, so a transient rpcd error retries on the next cycle instead of pinning
|
|
an empty widget list for the session.
|
|
|
|
## Widget-to-section mapping is positional and order-sensitive
|
|
|
|
There is no attribute tying a rendered section to the include that produced it,
|
|
so the mapping is by index: the *n*-th `.cbi-section` belongs to the *n*-th
|
|
include. `data-overview-widget` is stamped on first use and reused afterwards.
|
|
|
|
`index.js` requires its includes as `view.status.include.<name>` and sorts
|
|
**those module names**, so the `.js` suffix takes no part in the ordering. A
|
|
file-name sort is not the same sort: `-` is 0x2D and `.` is 0x2E, so plain file
|
|
names order `20_x-y.js` before `20_x.js`, while module names order `20_x` before
|
|
`20_x-y`. One such pair shifts every later widget onto the wrong section, and
|
|
the visible effect is that the wrong card is hidden or moved.
|
|
|
|
`common.compareWidgets` exists for exactly this and must stay code-unit based.
|
|
`localeCompare` would reorder per browser locale and reintroduce the bug in a
|
|
locale-dependent way.
|
|
|
|
Two further constraints follow from the same mechanism:
|
|
|
|
- The include count must equal the section count, or `applyLayout()` gives up
|
|
after eight retries and silently does nothing. `index.js` filters
|
|
`fs.list` entries by `type == 'file'`, so an include installed as a symlink
|
|
would be skipped there while the shell helper's `[ -f ]` follows it and counts
|
|
it. Nothing in the tree creates such a symlink today; if the layout ever stops
|
|
applying on a specific device, check this first.
|
|
- `title` must be non-empty and stable. `index.js` uses the include title as the
|
|
`localStorage` key for its own per-browser Hide button (`includes[i].id =
|
|
title`), so an empty title claims the empty key and collides with any other
|
|
untitled include. It is deliberately not wrapped in `_()`: a translated title
|
|
would change that key whenever the interface language changes.
|
|
|
|
## Client-side translation
|
|
|
|
The chain is not obvious and is easy to get subtly wrong.
|
|
|
|
- The global `_()` is defined in `luci-base/htdocs/luci-static/resources/cbi.js`,
|
|
not in `luci.js`. It is `TR[sfh(trimws(s))] || s`.
|
|
- `window.TR` is emitted by `admin/translations/<lang>`
|
|
(`luci-base/ucode/controller/admin/index.uc`), which the theme header loads on
|
|
every page, including the login page. It is a flat map of eight-hex-digit
|
|
hashes to strings, merged from **every** `*.<lang>.lmo` in
|
|
`/usr/lib/lua/luci/i18n`.
|
|
- The lookup key is the hash of the *whitespace-normalised* msgid: `trimws` in
|
|
`cbi.js` trims and collapses runs of space, tab and newline. `po2lmo` hashes
|
|
the **raw** msgid from the catalog. **Therefore msgids must already be
|
|
normalised** — one line, single spaces, no leading or trailing whitespace — or
|
|
the runtime lookup misses and the string silently stays English.
|
|
- Because all catalogs share one namespace in an unspecified merge order, a
|
|
string that `luci-base` also translates must either be left out or given the
|
|
identical translation. `Show`, `Move up` and `Move down` are in that category
|
|
and use the exact `luci-base` wording so the collision cannot matter.
|
|
|
|
`scripts/test-po2lmo.py` enforces the normalisation rule, the catalog-to-source
|
|
correspondence in both directions, and round-trips every translation through the
|
|
real hash and a minimal LMO reader.
|
|
|
|
### Registering the language is mandatory
|
|
|
|
Shipping a catalog is not enough. `determine_request_language()` in
|
|
`luci-base/ucode/dispatcher.uc` only accepts a language that exists in
|
|
`luci.languages`; with `luci.main.lang=auto` and no matching entry it falls
|
|
through to `en`. A router with no `luci-i18n-*` package installed has an empty
|
|
`luci.languages`, so the catalog would never be selected.
|
|
|
|
`openwrt/files/etc/uci-defaults/luci-app-overview-manager` adds the entry, the
|
|
same way `luci.mk` generates it for stock `luci-i18n-*` packages. Because this
|
|
package defines its own `postinst`, the default handler that drains
|
|
`/etc/uci-defaults` does not run, so `postinst` invokes the script itself and
|
|
removes it on success; otherwise the language would only appear after the next
|
|
reboot.
|
|
|
|
Verify on a device:
|
|
|
|
```sh
|
|
wget -qO- 'http://127.0.0.1/cgi-bin/luci/admin/translations/ru' | head -c 400
|
|
uci show luci.languages
|
|
```
|
|
|
|
and that content negotiation resolves as expected:
|
|
|
|
```sh
|
|
curl -s -H 'Accept-Language: ru' http://<router>/cgi-bin/luci/ | grep -o '<html lang="[^"]*"'
|
|
```
|
|
|
|
### `scripts/po2lmo.py` must stay byte-compatible
|
|
|
|
It reimplements `luci-base/src/po2lmo.c`. Format details that are easy to lose:
|
|
values are written first, each padded to a four-byte boundary; the index of
|
|
16-byte big-endian records follows, sorted by key hash; the file ends with the
|
|
total size of the value blob. The `val_id` field does not hold a hash, it holds
|
|
the plural count. Entries whose key and value hash identically are omitted so
|
|
the reader falls back to the source string. `extract_string` unescapes only `\"`
|
|
and `\\` — `\n` stays two literal characters, which is what makes the header's
|
|
`Plural-Forms` parsing work. An empty catalog produces no file at all.
|
|
|
|
To re-verify after any change, build the reference tool and diff its output. The
|
|
reader needs a generated parser header that is not required for writing, so an
|
|
empty stub is enough:
|
|
|
|
```sh
|
|
git clone --depth 1 --filter=blob:none --no-checkout https://github.com/openwrt/luci
|
|
cd luci && git sparse-checkout set modules/luci-base && git checkout master
|
|
cd .. && mkdir -p oracle/lib
|
|
cp luci/modules/luci-base/src/po2lmo.c oracle/
|
|
sed -n '1,72p' luci/modules/luci-base/src/lib/lmo.c > oracle/lib/sfh.c
|
|
cp luci/modules/luci-base/src/lib/lmo.h oracle/lib/
|
|
: > oracle/lib/plural_formula.h
|
|
cc -O1 -Ioracle/lib -o oracle/po2lmo oracle/po2lmo.c oracle/lib/sfh.c
|
|
|
|
PO2LMO_REFERENCE=$PWD/oracle/po2lmo python3 scripts/test-po2lmo.py
|
|
```
|
|
|
|
The same binary can be diffed against every catalog in `luci-base/po/*/*.po` as
|
|
a broader check; all of them currently match byte for byte.
|
|
|
|
## Static resources are cached without a cache-buster
|
|
|
|
`L.require` builds module URLs as
|
|
`<base>/<path>.js` plus `?v=<env.resource_version>` **only when that field is
|
|
set**, and the OpenWrt 25.12 page template does not set it — it versions
|
|
`luci.js` alone. uhttpd serves the files with `ETag` and `Last-Modified` but no
|
|
`Cache-Control`, so browsers apply heuristic freshness and can reuse a stale
|
|
copy for a long time.
|
|
|
|
Practical effect: after changing anything under
|
|
`/www/luci-static/resources/`, a normal reload may keep running the old code. A
|
|
reload that bypasses the cache is required — in Safari that is
|
|
`Cmd+Option+R` ("Reload Page From Origin"); `Cmd+Shift+R` does nothing there.
|
|
Confirm which code is live by reading the section titles, since a stale include
|
|
renders its section heading differently:
|
|
|
|
```js
|
|
[...document.querySelectorAll('.cbi-section')].map(s => s.querySelector('h3')?.textContent.trim())
|
|
```
|
|
|
|
## Shell that ships to the router runs under BusyBox
|
|
|
|
The test suite runs on GNU coreutils, which silently accepts constructs a stock
|
|
OpenWrt image does not have: `sort -o`, `base64`, `grep -P` and `find -printf`
|
|
among them. `scripts/check-busybox-compat.sh` scans the **staged package tree**
|
|
rather than the sources, so the checked set cannot drift from what is installed;
|
|
it also runs `busybox ash -n` on each script when a `busybox` binary is present,
|
|
which no pattern list can replace.
|
|
|
|
Four files reach the router: `CONTROL/postinst`, `CONTROL/prerm`,
|
|
`etc/uci-defaults/luci-app-overview-manager` and `usr/libexec/overview-manager`.
|
|
|
|
## Working against a real device
|
|
|
|
- OpenWrt has no `sftp-server`, so `scp` fails. Stream instead:
|
|
`ssh <host> 'cat > /path' < localfile`.
|
|
- `apk-tools` on the device has no `mkpkg`, and OpenWrt 25.12 cannot install an
|
|
`.ipk`. Building a package needs the SDK; for a quick check, deploy the payload
|
|
files and compare checksums.
|
|
- ACL changes need `/etc/init.d/rpcd reload`, which drops active LuCI sessions.
|
|
- Name the package in every transaction. A blanket upgrade is never appropriate
|
|
on a live router; `scripts/check-apk-trust.sh` fails the build if one appears
|
|
in a script or in the documentation.
|
|
- Verify a published package with the device's own `apk` and the tracked public
|
|
key, and confirm it is rejected without it:
|
|
|
|
```sh
|
|
apk verify --keys-dir <dir-with-public-key> <package>.apk
|
|
```
|