🌴 Sync 2026-07-07 04:15:13

This commit is contained in:
github-actions[bot] 2026-07-07 04:15:13 +08:00
parent 2a528e3190
commit 5c17703277
33 changed files with 464 additions and 419 deletions

View File

@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=dnsproxy
PKG_VERSION:=0.82.1
PKG_RELEASE:=12
PKG_VERSION:=0.83.0
PKG_RELEASE:=13
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/dnsproxy/tar.gz/v$(PKG_VERSION)?

View File

@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.7
PKG_RELEASE:=28
PKG_RELEASE:=29
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)

View File

@ -677,9 +677,24 @@ return view.extend({
};
const importDirectNodes = function() {
const existing = {};
const usedTags = {};
(((before || {}).nodes || {}).edges || []).forEach(function(node) {
existing[clashConverter.normalizeLink(node.link)] = node;
if (node.tag)
usedTags[String(node.tag)] = true;
});
const readableNodeTag = function(item, index) {
const base = String(item.name || '').replace(/[\x00-\x1f\x7f]+/g, ' ').replace(/\s+/g, ' ').trim()
|| _('Node %d').format(index + 1);
let tag = base.slice(0, 96);
let suffix = 2;
while (usedTags[tag]) {
const tail = ' ' + suffix++;
tag = base.slice(0, 96 - tail.length) + tail;
}
usedTags[tag] = true;
return tag;
};
const fresh = items.filter(function(item) {
const duplicate = existing[clashConverter.normalizeLink(item.link)];
if (duplicate)
@ -689,7 +704,7 @@ return view.extend({
const imported = fresh.length
? graphQL(endpoint,
'mutation ImportNodes($args:[ImportArgument!]!){importNodes(rollbackError:false,args:$args){link error node{id}}}',
{ args: fresh.map(function(item, index) { return { link: item.link, tag: airportSync.backendId(airportId) + Date.now().toString(36) + (index + 1) }; }) }, token)
{ args: fresh.map(function(item, index) { return { link: item.link, tag: readableNodeTag(item, index) }; }) }, token)
: Promise.resolve({ importNodes: [] });
return imported.then(function(result) {

View File

@ -6,7 +6,7 @@
'require tools.widgets as widgets';
/*
* Copyright (C) 2024-2025 Ycarus (Yannick Chabanois) <contact@openmptcprouter.com> for OpenMPTCProuter
* Copyright (C) 2024-2026 Ycarus (Yannick Chabanois) <contact@openmptcprouter.com> for OpenMPTCProuter
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information
*/
@ -276,30 +276,11 @@ return L.view.extend({
//o.value("handover", _("handover"));
o.default = "off";
o = s.option(form.Value, "multipath_weight", _("Weight"), _("Only for *weight schedulers/path managers (if any available)") + '<br />' + _("A weight >100 make it more attractive, a weight <100 make it less attractive. Max 256"));
o = s.option(form.Value, "multipath_weight", _("Weight"), _("Only used by *weight schedulers/path managers. Ignored if no weight scheduler is selected.") + '<br />' + _("A weight >100 make it more attractive, a weight <100 make it less attractive. Max 256"));
o.datatype = "uinteger";
o.rmempty = false;
o.default = 100;
return m.render().then(function(mapEl) {
function updateWeightVisibility() {
var schedulerEl = document.getElementById('widget.cbid.network.globals.mptcp_scheduler');
var val = schedulerEl ? schedulerEl.value : (uci.get('network', 'globals', 'mptcp_scheduler') || '');
var show = val.toLowerCase().indexOf('weight') > -1;
mapEl.querySelectorAll('[data-name="multipath_weight"]').forEach(function(el) {
var row = el.closest('.cbi-value') || el;
row.style.display = show ? '' : 'none';
});
}
var schedulerEl = document.getElementById('widget.cbid.network.globals.mptcp_scheduler');
if (schedulerEl) {
schedulerEl.addEventListener('change', updateWeightVisibility);
}
updateWeightVisibility();
return mapEl;
});
return m.render();
}
});

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.7.1
PKG_RELEASE:=173
PKG_RELEASE:=174
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -179,9 +179,9 @@ insert_nftset() {
printf "%s\n" "$@"
else
cat
fi | tr -s ' \t' '\n' | awk -v s="$suffix" -v n="$nftset_name" -v t="$NFTABLE_NAME" '
{
gsub(/^[ \t\r]+|[ \t\r]+$/, "");
fi | awk -v s="$suffix" -v n="$nftset_name" -v t="$NFTABLE_NAME" '
BEGIN {
RS = "[ \t\n\r]+"
}
$0 != "" {
if (first == 0) {

View File

@ -4,7 +4,7 @@
"read": {
"file": {
"/etc/samba/smb.conf.template": [ "read" ],
"/usr/sbin/smbd": [ "exec" ]
"/usr/sbin/smbd -V": [ "exec" ]
},
"uci": [ "samba4" ]
},

View File

@ -69,7 +69,7 @@ return baseclass.extend({
rule.extport,
rule.proto,
expires_str,
rule.descr,
'%h'.format(rule.descr),
E('button', {
'class': 'btn cbi-button-remove',
'click': L.bind(handleDelRule, this, rule.num),

View File

@ -66,7 +66,7 @@ return view.extend({
rule.extport,
rule.proto,
expires_str,
rule.descr,
'%h'.format(rule.descr),
E('button', {
'class': 'btn cbi-button-remove',
'click': L.bind(handleDelRule, this, rule.num),

View File

@ -1,17 +1,12 @@
# OpenWrt LuCI web UI address
VITE_OPENWRT_HOST=http://192.168.1.1
# Optional — run `pnpm setup` once and you usually don't need a .env at all.
# SSH config for syncing .ut template files to the device on change
# Leave SSH_HOST empty to disable ut sync.
VITE_OPENWRT_SSH_HOST=root@192.168.1.1
#
# Path to SSH private key (optional, falls back to ssh-agent or ~/.ssh/config)
# VITE_OPENWRT_SSH_KEY=~/.ssh/id_rsa
# OpenWrt router address — bare IP/hostname; the web proxy target and the
# .ut-sync ssh target both derive from it (ssh key selection etc. belongs
# in ~/.ssh/config)
# VITE_OPENWRT_HOST=192.168.1.1
# Development server configuration
# Dev server bind / port
VITE_DEV_PORT=5173
# Specify which IP addresses the server should listen on
# Set to 0.0.0.0 to listen on all addresses, including LAN and public addresses
# Set to 127.0.0.1 to listen only on local machine
VITE_DEV_HOST=0.0.0.0
# 0.0.0.0 = LAN; 127.0.0.1 = local only
VITE_DEV_HOST=0.0.0.0

View File

@ -28,18 +28,16 @@ pnpm install
### 2. Configure Environment
```bash
# Copy environment template
cp .env.example .env
# Edit .env and set your OpenWrt device address
# VITE_OPENWRT_HOST=http://192.168.1.1
# One-shot wizard: asks for the router IP, generates/installs an SSH key
# (one router-password prompt), and writes .env
pnpm setup
```
**Environment Variables:**
If the router is at the default `192.168.1.1` and passwordless SSH already works, no `.env` is needed at all — every value below has a working default.
- `VITE_OPENWRT_HOST` - Your OpenWrt LuCI web interface URL (required)
- `VITE_OPENWRT_SSH_HOST` - SSH target for `.ut` template sync, e.g. `root@192.168.1.1` (optional)
- `VITE_OPENWRT_SSH_KEY` - Path to SSH private key (optional, falls back to ssh-agent or `~/.ssh/config`)
**Environment Variables** (all optional):
- `VITE_OPENWRT_HOST` - bare router address, e.g. `192.168.1.1` (default; `host:port` and full-URL forms also accepted). The web proxy target and the `.ut`-sync SSH target (`root@<hostname>`) both derive from it; anything fancier (a dedicated key, a jump host, a non-standard ssh port) belongs in a `Host` block in `~/.ssh/config`, which ssh picks up automatically.
- `VITE_DEV_HOST` - Development server host (code default: `127.0.0.1`, `.env.example` sets `0.0.0.0` for LAN access)
- `VITE_DEV_PORT` - Development server port (default: `5173`)
@ -94,7 +92,7 @@ Third-party compatibility patches are **not** bundled into `main.css` — they a
**Adding new styles:**
- New UI component → create `components/_<name>.css` and add an `@import` line to `main.css`. Each file is its own organizational unit — no `@layer` wrappers needed (any that remain are stripped by PostCSS).
- New UI component → create `components/_<name>.css` and add an `@import` line to `main.css`. Each file is its own organizational unit — don't add `@layer` wrappers: theme partials stay unlayered, so they outrank Tailwind's layered base/utilities regardless of specificity.
- Compatibility fix for a third-party LuCI app/page → add a new file under `media/patches/` (see below).
All rules use `@apply` with Tailwind utilities and CSS Nesting — no raw CSS properties.
@ -184,46 +182,23 @@ For LuCI-specific JavaScript development, refer to the official API documentatio
- **CSS changes**: Trigger full page reload via custom HMR handler
- **JS changes**: Trigger full page reload via custom HMR handler
- **Template changes** (`.ut` files): Auto-synced to router via SCP and trigger full page reload (requires SSH setup, see below)
- **Template changes** (`.ut` files): Auto-synced to router over SSH and trigger full page reload (one-time `pnpm setup` required, see below)
### Template (`.ut`) Live Sync
The `.ut` template files are rendered server-side on the OpenWrt device. To see template changes during development, the dev server can automatically sync modified `.ut` files to the router via SCP.
The `.ut` template files are rendered server-side on the OpenWrt device, so unlike CSS/JS they can't be served locally — the dev server pushes them to the router instead. Run `pnpm setup` once to configure passwordless SSH; after that it's fully automatic:
**1. Set up SSH key authentication to your router:**
- **On startup**, the whole template directory is pushed (as one tarball over ssh stdin — Dropbear has no SFTP server for scp), so edits made while the dev server was down never leave the router stale.
- **On save**, changes are debounced and the directory is pushed again, then the browser reloads.
- **On page load**, requests to `/cgi-bin` wait for any in-flight push, so a proxied render never uses a stale template.
```bash
# Generate a key if you don't have one
ssh-keygen -t ed25519
# Copy your public key to the router (OpenWrt uses Dropbear, not OpenSSH)
cat ~/.ssh/id_ed25519.pub | ssh root@192.168.1.1 "cat >> /etc/dropbear/authorized_keys"
# Verify passwordless login works
ssh root@192.168.1.1 "echo ok"
```
**2. Add SSH config to `.env`:**
```bash
# SSH target for .ut file sync (user@host)
VITE_OPENWRT_SSH_HOST=root@192.168.1.1
# Optional: path to SSH private key (falls back to ssh-agent or ~/.ssh/config)
# VITE_OPENWRT_SSH_KEY=~/.ssh/id_ed25519
```
**3. Start `pnpm dev` and edit any `.ut` file** — the dev server will automatically sync it to the router and reload the browser.
**Troubleshooting:**
The dev server checks SSH connectivity on startup and prints actionable errors:
**Troubleshooting** — sync errors are printed with the fix:
- **Host key mismatch** (device was reflashed): Run `ssh-keygen -R <device-ip>`, then restart the dev server
- **Authentication failed** (public key not on device): Copy your key with the command above
- **Authentication failed** (public key not on device): Run `pnpm setup`
- **Connection refused/timed out**: Check that the device is online and SSH is enabled
If `VITE_OPENWRT_SSH_HOST` is not set, template sync is simply disabled and other dev features work normally.
A failed sync is retried on the next `.ut` change; CSS/JS dev features work normally without SSH.
## Building for Production
@ -252,10 +227,9 @@ htdocs/luci-static/
**Build Process:**
1. `pnpm gen:tokens` regenerates `src/media/_tokens.css` from `tokens/` (see [Design Tokens](#design-tokens))
2. Vite builds the CSS entry points (`src/media/main.css` and `src/media/login.css`)
3. Custom PostCSS plugin removes `@layer` at-rules for OpenWrt compatibility
4. Custom Vite plugin (`luci-js-compress`) minifies JS files via Terser
5. Static assets copied from `.dev/public/aurora/`
2. Vite builds the CSS entry points (`src/media/main.css` and `src/media/login.css`), keeping Tailwind's native `@layer` structure
3. Custom Vite plugin (`luci-js-compress`) minifies JS files via Terser
4. Static assets copied from `.dev/public/aurora/`
## Package Compilation

View File

@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"setup": "node scripts/setup.js",
"build": "npm run clean && npm run gen:tokens && vite build",
"clean": "node scripts/clean.js",
"gen:tokens": "node scripts/gen-tokens.js",

View File

@ -0,0 +1,93 @@
/**
* One-shot dev environment setup: writes .env and installs an SSH key on the
* router so ut-sync works without any manual ssh configuration.
*
* Usage: pnpm setup (asks for the router IP, then one router password prompt)
*/
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { createInterface } from "node:readline/promises";
const SSH_OPTS = [
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
];
function canAuth(host) {
return (
spawnSync(
"ssh",
[...SSH_OPTS, "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", host, "echo ok"],
{ stdio: "ignore" },
).status === 0
);
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ip =
(await rl.question("Router IP [192.168.1.1]: ")).trim() || "192.168.1.1";
rl.close();
const host = `root@${ip}`;
const sshDir = join(homedir(), ".ssh");
let pubKeyPath = ["id_ed25519.pub", "id_rsa.pub", "id_ecdsa.pub"]
.map((f) => join(sshDir, f))
.find((f) => existsSync(f));
if (!pubKeyPath) {
console.log("No SSH key found — generating ~/.ssh/id_ed25519 ...");
const keygen = spawnSync(
"ssh-keygen",
["-t", "ed25519", "-N", "", "-f", join(sshDir, "id_ed25519")],
{ stdio: "inherit" },
);
if (keygen.status !== 0) {
console.error("✗ ssh-keygen failed.");
process.exit(1);
}
pubKeyPath = join(sshDir, "id_ed25519.pub");
}
if (canAuth(host)) {
console.log(`✓ Passwordless SSH to ${host} already works.`);
} else {
console.log(
`Installing ${pubKeyPath} on the router (enter the router password once):`,
);
const pubKey = readFileSync(pubKeyPath, "utf-8").trim();
const install = spawnSync(
"ssh",
[
...SSH_OPTS,
host,
`mkdir -p /etc/dropbear && echo '${pubKey}' >> /etc/dropbear/authorized_keys && chmod 600 /etc/dropbear/authorized_keys`,
],
{ stdio: "inherit" },
);
if (install.status !== 0 || !canAuth(host)) {
console.error(
`✗ Key install failed. Check that the device at ${ip} is reachable and the password is correct, then re-run pnpm setup.`,
);
process.exit(1);
}
console.log("✓ Passwordless SSH configured.");
}
const envPath = resolve(process.cwd(), ".env");
const kept = existsSync(envPath)
? readFileSync(envPath, "utf-8")
.split("\n")
.filter((l) => l && !l.startsWith("VITE_OPENWRT_HOST="))
: [];
writeFileSync(
envPath,
[`VITE_OPENWRT_HOST=${ip}`, ...kept].join("\n") + "\n",
);
console.log(`✓ Wrote .env (VITE_OPENWRT_HOST=${ip})`);
console.log("Done — run `pnpm dev`.");

View File

@ -18,118 +18,116 @@
}
}
@layer login {
.login-screen {
@apply relative flex min-h-dvh flex-col items-center justify-center px-5 pb-14;
}
.login-screen {
@apply relative flex min-h-dvh flex-col items-center justify-center px-5 pb-14;
}
.login-shell {
@apply w-full max-w-md;
}
.login-shell {
@apply w-full max-w-md;
}
/* Fixed sibling layer (not form ancestor) on its own GPU surface so input
paints don't re-sample the bg image. DOM order keeps the form on top. */
/* LQIP shown immediately; falls back to full image when no LQIP stored */
.login-bg {
@apply pointer-events-none fixed inset-0 transform-[translateZ(0)] bg-cover bg-center bg-no-repeat [background-image:var(--login-bg-lqip,var(--login-bg))];
}
/* Fixed sibling layer (not form ancestor) on its own GPU surface so input
paints don't re-sample the bg image. DOM order keeps the form on top. */
/* LQIP shown immediately; falls back to full image when no LQIP stored */
.login-bg {
@apply pointer-events-none fixed inset-0 transform-[translateZ(0)] bg-cover bg-center bg-no-repeat [background-image:var(--login-bg-lqip,var(--login-bg))];
}
/* Full image fades in on top once loaded (compositor-only opacity change) */
.login-bg::after {
@apply pointer-events-none absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 [will-change:opacity] content-[''] [transition:opacity_0.5s_ease] [background-image:var(--login-bg)];
}
/* Full image fades in on top once loaded (compositor-only opacity change) */
.login-bg::after {
@apply pointer-events-none absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 [will-change:opacity] content-[''] [transition:opacity_0.5s_ease] [background-image:var(--login-bg)];
}
.login-bg.full-loaded::after {
@apply opacity-100;
}
.login-bg.full-loaded::after {
@apply opacity-100;
}
.login-card {
@apply bg-surface flex flex-col gap-7 rounded-4xl p-14 shadow-lg max-md:rounded-3xl max-md:p-9;
}
.login-card {
@apply bg-surface flex flex-col gap-7 rounded-4xl p-14 shadow-lg max-md:rounded-3xl max-md:p-9;
}
.login-brand {
@apply flex items-center gap-3.5;
}
.login-brand {
@apply flex items-center gap-3.5;
}
.login-divider {
@apply via-hairline h-px w-full animate-[divider-in_0.7s_cubic-bezier(0.16,1,0.3,1)_0.1s_both] border-0 bg-linear-to-r from-transparent to-transparent;
}
.login-divider {
@apply via-hairline h-px w-full animate-[divider-in_0.7s_cubic-bezier(0.16,1,0.3,1)_0.1s_both] border-0 bg-linear-to-r from-transparent to-transparent;
}
.login-logo {
@apply bg-brand-subtle ring-brand-subtle flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl ring-1;
.login-logo {
@apply bg-brand-subtle ring-brand-subtle flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl ring-1;
& img {
@apply h-6 w-6;
}
}
.login-title {
@apply flex flex-col gap-1;
}
.login-hostname {
@apply text-text text-xl font-semibold tracking-tight;
}
.login-subtitle {
@apply text-text text-sm;
}
.login-form {
@apply flex flex-col gap-5;
}
.login-fields {
@apply flex flex-col gap-4;
}
.login-field {
@apply flex flex-col gap-2;
}
.login-label {
@apply text-text text-xs font-semibold tracking-widest uppercase;
}
.input-wrap {
@apply border-hairline bg-surface-overlay dark:bg-surface-sunken relative rounded-2xl border [contain:layout_style];
}
/* Focus ring: opacity-only transition runs on compositor thread, not main thread
border-color changes require a repaint (~400 ms INP); opacity changes do not. */
.input-wrap::after {
@apply border-brand pointer-events-none absolute -inset-px rounded-[calc(var(--radius-2xl)+1px)] border-2 opacity-0 [will-change:opacity] content-[''] [transition:opacity_150ms_ease];
}
.input-wrap:focus-within::after {
@apply opacity-100;
}
.login-input {
@apply text-text placeholder:text-text-subtle w-full appearance-none rounded-2xl border-0 bg-transparent px-3 py-2.5 text-base outline-none;
}
.login-error {
@apply bg-danger-surface text-danger border-hairline rounded-2xl border px-3 py-2 text-center text-sm;
}
.login-actions {
@apply pt-4;
}
.login-submit {
@apply bg-brand text-on-brand w-full cursor-pointer rounded-2xl border-0 py-2.5 text-base font-medium hover:opacity-90 active:opacity-80;
}
.login-footer {
@apply text-text-muted absolute right-0 bottom-5 left-0 px-5 text-center text-xs;
& p {
@apply whitespace-normal;
}
& a {
@apply text-brand hover:underline;
}
& img {
@apply h-6 w-6;
}
}
.login-title {
@apply flex flex-col gap-1;
}
.login-hostname {
@apply text-text text-xl font-semibold tracking-tight;
}
.login-subtitle {
@apply text-text text-sm;
}
.login-form {
@apply flex flex-col gap-5;
}
.login-fields {
@apply flex flex-col gap-4;
}
.login-field {
@apply flex flex-col gap-2;
}
.login-label {
@apply text-text text-xs font-semibold tracking-widest uppercase;
}
.input-wrap {
@apply border-hairline bg-surface-overlay dark:bg-surface-sunken relative rounded-2xl border [contain:layout_style];
}
/* Focus ring: opacity-only transition runs on compositor thread, not main thread
border-color changes require a repaint (~400 ms INP); opacity changes do not. */
.input-wrap::after {
@apply border-brand pointer-events-none absolute -inset-px rounded-[calc(var(--radius-2xl)+1px)] border-2 opacity-0 [will-change:opacity] content-[''] [transition:opacity_150ms_ease];
}
.input-wrap:focus-within::after {
@apply opacity-100;
}
.login-input {
@apply text-text placeholder:text-text-subtle w-full appearance-none rounded-2xl border-0 bg-transparent px-3 py-2.5 text-base outline-none;
}
.login-error {
@apply bg-danger-surface text-danger border-hairline rounded-2xl border px-3 py-2 text-center text-sm;
}
.login-actions {
@apply pt-4;
}
.login-submit {
@apply bg-brand text-on-brand w-full cursor-pointer rounded-2xl border-0 py-2.5 text-base font-medium hover:opacity-90 active:opacity-80;
}
.login-footer {
@apply text-text-muted absolute right-0 bottom-5 left-0 px-5 text-center text-xs;
& p {
@apply whitespace-normal;
}
& a {
@apply text-brand hover:underline;
}
}

View File

@ -6,7 +6,7 @@
import tailwindcss from "@tailwindcss/vite";
import browserslist from "browserslist";
import { exec } from "child_process";
import { watch as fsWatch, readdirSync } from "fs";
import { existsSync, readdirSync } from "fs";
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
import { browserslistToTargets } from "lightningcss";
import { basename, dirname, join, relative, resolve } from "path";
@ -59,35 +59,36 @@ function createLuciJsCompressPlugin(): Plugin {
},
async generateBundle() {
for (const filePath of jsFiles) {
try {
const sourceCode = await readFile(filePath, "utf-8");
const compressed = await terserMinify(sourceCode, {
parse: { bare_returns: true },
compress: false,
mangle: false,
format: { comments: false, beautify: false },
});
await Promise.all(
jsFiles.map(async (filePath) => {
try {
const sourceCode = await readFile(filePath, "utf-8");
const compressed = await terserMinify(sourceCode, {
parse: { bare_returns: true },
compress: false,
mangle: false,
format: { comments: false, beautify: false },
});
const relativePath = relative(
resolve(CURRENT_DIR, "src/resource"),
filePath,
).replace(/\\/g, "/");
const outputPath = join(outDir, "resources", relativePath);
const relativePath = relative(
resolve(CURRENT_DIR, "src/resource"),
filePath,
).replace(/\\/g, "/");
const outputPath = join(outDir, "resources", relativePath);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, compressed.code || sourceCode, "utf-8");
} catch (error: any) {
console.error(`JS compress failed: ${filePath}`, error?.message);
}
}
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, compressed.code || sourceCode, "utf-8");
} catch (error: any) {
console.error(`JS compress failed: ${filePath}`, error?.message);
}
}),
);
},
};
}
interface RouteConfig {
routes: Record<string, string>;
shouldRewrite: boolean;
hmrMessage: string;
}
@ -96,18 +97,13 @@ interface ResourceConfig {
js: RouteConfig;
}
// On-demand third-party patches: serve each src/media/patches/<page>.css at
// On-demand third-party patches: serve src/media/patches/<page>.css at
// /luci-static/aurora/patches/<page>.css in dev. Without this, header.ut's patch
// <link> falls through to the OpenWrt proxy (404 / stale router asset) and patch
// edits don't trigger HMR. Mirrors the build entries derived from the same dir.
function patchCssRoutes(): Record<string, string> {
const dir = resolve(CURRENT_DIR, "src/media/patches");
return Object.fromEntries(
readdirSync(dir)
.filter((f) => f.endsWith(".css"))
.map((f) => [`/luci-static/aurora/patches/${f}`, `/src/media/patches/${f}`]),
);
}
// edits don't trigger HMR. Matched per request so new patch files work without
// a dev-server restart.
const PATCH_PUBLIC_PREFIX = "/luci-static/aurora/patches/";
const PATCH_SRC_DIR = resolve(CURRENT_DIR, "src/media/patches");
function createLocalServePlugin(): Plugin {
const resourceConfig: ResourceConfig = {
@ -115,9 +111,7 @@ function createLocalServePlugin(): Plugin {
routes: {
"/luci-static/aurora/main.css": "/src/media/main.css",
"/luci-static/aurora/login.css": "/src/media/login.css",
...patchCssRoutes(),
},
shouldRewrite: true,
hmrMessage: "CSS file changed",
},
js: {
@ -126,7 +120,6 @@ function createLocalServePlugin(): Plugin {
"src/resource/view/aurora/sysauth.js",
"/luci-static/resources/menu-aurora.js": "src/resource/menu-aurora.js",
},
shouldRewrite: false,
hmrMessage: "JS file changed",
},
};
@ -162,6 +155,14 @@ function createLocalServePlugin(): Plugin {
return next();
}
if (pathname.startsWith(PATCH_PUBLIC_PREFIX) && pathname.endsWith(".css")) {
const file = basename(pathname);
if (existsSync(join(PATCH_SRC_DIR, file))) {
req.url = `/src/media/patches/${file}` + (search ? `?${search}` : "");
return next();
}
}
const jsPath = resourceConfig.js.routes[pathname];
if (jsPath) {
try {
@ -184,6 +185,15 @@ function createLocalServePlugin(): Plugin {
handleHotUpdate({ file, server }) {
const normalizedFile = file.replace(/\\/g, "/");
if (
normalizedFile.startsWith(PATCH_SRC_DIR.replace(/\\/g, "/") + "/") &&
normalizedFile.endsWith(".css")
) {
console.log(`[HMR] Patch CSS changed: ${basename(normalizedFile)}`);
server.ws.send({ type: "full-reload", path: "*" });
return [];
}
const resources = [
{ map: cssHmrMap, config: resourceConfig.css },
{ map: jsHmrMap, config: resourceConfig.js },
@ -204,113 +214,116 @@ function createLocalServePlugin(): Plugin {
const UT_TEMPLATE_DIR = resolve(PROJECT_ROOT, "ucode/template/themes/aurora");
const UT_REMOTE_DIR = "/usr/share/ucode/luci/template/themes/aurora";
interface ScpConfig {
host: string;
key?: string;
}
function buildSshArgs(cfg: ScpConfig): string {
const args = ["-o StrictHostKeyChecking=no", "-o UserKnownHostsFile=/dev/null"];
if (cfg.key) args.push(`-i "${cfg.key}"`);
return args.join(" ");
}
// Stream the file over ssh stdin instead of scp: OpenSSH 9+ scp defaults to
// the SFTP protocol, which Dropbear on OpenWrt does not ship a server for.
function buildScpCommand(localPath: string, remotePath: string, cfg: ScpConfig): string {
return `ssh ${buildSshArgs(cfg)} "${cfg.host}" "cat > '${remotePath}'" < "${localPath}"`;
}
// Key selection is ssh's own job: ssh-agent or a Host block in ~/.ssh/config.
const SSH_ARGS = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
function parseHost(sshHost: string): string {
const atIndex = sshHost.lastIndexOf("@");
return atIndex !== -1 ? sshHost.slice(atIndex + 1) : sshHost;
}
async function checkSshConnection(cfg: ScpConfig): Promise<boolean> {
const host = parseHost(cfg.host);
function reportSshError(err: any, sshHost: string): void {
const host = parseHost(sshHost);
const stderr = err?.stderr || err?.message || "";
if (stderr.includes("Host key verification failed") || stderr.includes("REMOTE HOST IDENTIFICATION HAS CHANGED")) {
console.error(`\n[UT Sync] SSH host key mismatch for ${host}.`);
console.error(`[UT Sync] The device may have been reflashed. Run this to fix:\n`);
console.error(` ssh-keygen -R ${host}\n`);
} else if (stderr.includes("Permission denied") || stderr.includes("Authentication failed")) {
console.error(`\n[UT Sync] SSH authentication failed for ${sshHost}.`);
console.error(`[UT Sync] Run \`pnpm setup\` to configure passwordless login.\n`);
} else if (stderr.includes("Connection refused") || stderr.includes("Connection timed out") || stderr.includes("No route to host")) {
console.error(`\n[UT Sync] Cannot reach ${host}. Check that the device is online and SSH is enabled.\n`);
} else {
console.error(`\n[UT Sync] SSH connection failed: ${stderr}\n`);
}
}
async function checkSshConnection(sshHost: string): Promise<boolean> {
try {
await execAsync(`ssh ${buildSshArgs(cfg)} -o ConnectTimeout=5 "${cfg.host}" echo ok`);
await execAsync(`ssh ${SSH_ARGS} -o ConnectTimeout=5 "${sshHost}" echo ok`);
console.log(`[UT Sync] SSH connection verified.`);
return true;
} catch (err: any) {
const stderr = err?.stderr || err?.message || "";
if (stderr.includes("Host key verification failed") || stderr.includes("REMOTE HOST IDENTIFICATION HAS CHANGED")) {
console.error(`\n[UT Sync] SSH host key mismatch for ${host}.`);
console.error(`[UT Sync] The device may have been reflashed. Run this to fix:\n`);
console.error(` ssh-keygen -R ${host}\n`);
console.error(`[UT Sync] Then restart the dev server.\n`);
} else if (stderr.includes("Permission denied") || stderr.includes("Authentication failed")) {
console.error(`\n[UT Sync] SSH authentication failed for ${cfg.host}.`);
console.error(`[UT Sync] Copy your public key to the device:\n`);
console.error(` cat ~/.ssh/id_ed25519.pub | ssh ${cfg.host} "cat >> /etc/dropbear/authorized_keys"\n`);
} else if (stderr.includes("Connection refused") || stderr.includes("Connection timed out") || stderr.includes("No route to host")) {
console.error(`\n[UT Sync] Cannot reach ${host}. Check that the device is online and SSH is enabled.\n`);
} else {
console.error(`\n[UT Sync] SSH connection failed: ${stderr}\n`);
}
reportSshError(err, sshHost);
return false;
}
}
function createUtSyncPlugin(cfg: ScpConfig): Plugin {
let syncing = false;
let pendingFile: string | null = null;
function createUtSyncPlugin(sshHost: string): Plugin {
let dirty = false;
let flushing: Promise<void> | null = null;
let timer: ReturnType<typeof setTimeout> | null = null;
function syncFile(filename: string, server: any) {
if (syncing) {
pendingFile = filename;
return;
// The templates are tiny (3 files), so every sync just pushes the whole
// directory in one tarball streamed over ssh stdin — OpenSSH 9+ scp defaults
// to the SFTP protocol, which Dropbear on OpenWrt does not ship a server for.
const pushAll = () =>
execAsync(
`tar -C "${UT_TEMPLATE_DIR}" -cf - . | ssh ${SSH_ARGS} "${sshHost}" "mkdir -p '${UT_REMOTE_DIR}' && tar -xf - -C '${UT_REMOTE_DIR}'"`,
);
const flush = (server: any): Promise<void> => {
if (!flushing) {
flushing = (async () => {
while (dirty) {
dirty = false;
try {
await pushAll();
console.log(`[UT Sync] Templates synced to ${sshHost}.`);
server.ws.send({ type: "full-reload", path: "*" });
} catch (err: any) {
reportSshError(err, sshHost);
break;
}
}
flushing = null;
})();
}
return flushing;
};
syncing = true;
pendingFile = null;
const filePath = join(UT_TEMPLATE_DIR, filename);
const remotePath = `${UT_REMOTE_DIR}/${filename}`;
const cmd = buildScpCommand(filePath, remotePath, cfg);
console.log(`[UT Sync] Syncing ${filename}${cfg.host}:${remotePath}`);
execAsync(cmd)
.then(() => {
console.log(`[UT Sync] Done. Reloading browser.`);
server.ws.send({ type: "full-reload", path: "*" });
})
.catch((err: any) => {
console.error(`[UT Sync] Failed to sync ${filename}:`, err?.message);
})
.finally(() => {
syncing = false;
if (pendingFile) syncFile(pendingFile, server);
});
}
const markDirty = (server: any) => {
dirty = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => flush(server), 150);
};
return {
name: "ut-sync-plugin",
apply: "serve",
configureServer(server) {
if (!cfg.host) {
console.log("[UT Sync] Disabled: VITE_OPENWRT_SSH_HOST not set in .env");
return;
}
const authInfo = cfg.key ? `key (${cfg.key})` : "ssh-agent/config";
console.log(`[UT Sync] Watching ${UT_TEMPLATE_DIR}`);
console.log(`[UT Sync] Target: ${cfg.host}:${UT_REMOTE_DIR} (auth: ${authInfo})`);
console.log(`[UT Sync] Target: ${sshHost}:${UT_REMOTE_DIR}`);
checkSshConnection(cfg).then((ok) => {
if (!ok) return;
// Full push on startup so edits made while the server was down (or a
// freshly flashed device) can't leave the router stale.
checkSshConnection(sshHost).then((ok) => {
if (ok) markDirty(server);
});
// Accept both "change" and "rename" — VS Code atomic saves emit "rename" on macOS
const watcher = fsWatch(UT_TEMPLATE_DIR, (eventType, filename) => {
if (!filename?.endsWith(".ut")) return;
syncFile(filename, server);
});
server.watcher.add(UT_TEMPLATE_DIR);
const onTemplateEvent = (file: string) => {
if (file.startsWith(UT_TEMPLATE_DIR) && file.endsWith(".ut")) {
markDirty(server);
}
};
server.watcher.on("add", onTemplateEvent);
server.watcher.on("change", onTemplateEvent);
server.httpServer?.on("close", () => watcher.close());
// Hold page loads until pending template pushes land, so a proxied
// render never uses a stale template.
server.middlewares.use((req, res, next) => {
if (!req.url?.startsWith("/cgi-bin") || (!dirty && !flushing)) {
return next();
}
if (timer) clearTimeout(timer);
flush(server).then(
() => next(),
() => next(),
);
});
},
};
@ -336,23 +349,36 @@ function createRedirectPlugin(): Plugin {
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, CURRENT_DIR, "");
const OPENWRT_HOST = env.VITE_OPENWRT_HOST || "http://192.168.1.1:80";
const OPENWRT_SSH_HOST = env.VITE_OPENWRT_SSH_HOST || "";
const OPENWRT_SSH_KEY = env.VITE_OPENWRT_SSH_KEY || "";
// VITE_OPENWRT_HOST is just the router address — a bare IP/hostname like
// 192.168.1.1 (host:port and http:// URL forms also work). The web proxy
// target and the .ut-sync ssh target are both derived from it; ssh key
// selection etc. belongs in ~/.ssh/config, not here.
const OPENWRT_RAW = env.VITE_OPENWRT_HOST || "192.168.1.1";
const OPENWRT = new URL(
/^https?:\/\//.test(OPENWRT_RAW) ? OPENWRT_RAW : `http://${OPENWRT_RAW}`,
);
const OPENWRT_URL = OPENWRT.origin;
const OPENWRT_SSH_HOST = `root@${OPENWRT.hostname}`;
const DEV_HOST = env.VITE_DEV_HOST || "127.0.0.1";
const DEV_PORT = Number(env.VITE_DEV_PORT) || 5173;
const proxyConfig = {
"/luci-static": {
target: OPENWRT_HOST,
target: OPENWRT_URL,
changeOrigin: true,
secure: false,
},
"/cgi-bin": {
target: OPENWRT_HOST,
target: OPENWRT_URL,
changeOrigin: true,
secure: false,
configure: (proxy: any) => {
// Force an uncompressed upstream response: the HTML injection below
// treats the body as UTF-8 text and would corrupt a gzipped payload.
proxy.on("proxyReq", (proxyReq: any) => {
proxyReq.removeHeader("accept-encoding");
});
proxy.on("proxyRes", (proxyRes: any, req: any, res: any) => {
const contentType = proxyRes.headers["content-type"] || "";
@ -395,7 +421,7 @@ export default defineConfig(({ mode }) => {
tailwindcss(),
createRedirectPlugin(),
createLocalServePlugin(),
createUtSyncPlugin({ host: OPENWRT_SSH_HOST, key: OPENWRT_SSH_KEY }),
createUtSyncPlugin(OPENWRT_SSH_HOST),
createLuciJsCompressPlugin(),
],
@ -405,22 +431,6 @@ export default defineConfig(({ mode }) => {
browserslist("last 4 versions, Firefox ESR, not dead"),
),
},
postcss: {
plugins: [
{
postcssPlugin: "remove-layers",
Once(root) {
function removeLayers(node: any) {
node.walkAtRules("layer", (rule: any) => {
removeLayers(rule);
rule.replaceWith(rule.nodes);
});
}
removeLayers(root);
},
},
],
},
},
build: {
@ -435,11 +445,11 @@ export default defineConfig(({ mode }) => {
// aurora/patches/<page>.css (the `patches/` key prefix lands them there
// via assetFileNames below). header.ut links the matching one per page.
...Object.fromEntries(
readdirSync(resolve(CURRENT_DIR, "src/media/patches"))
readdirSync(PATCH_SRC_DIR)
.filter((f) => f.endsWith(".css"))
.map((f) => [
`patches/${f.slice(0, -4)}`,
resolve(CURRENT_DIR, "src/media/patches", f),
join(PATCH_SRC_DIR, f),
]),
),
},

View File

@ -8,6 +8,7 @@ All dev commands run from `.dev/`:
```bash
cd .dev/
pnpm setup # One-shot dev setup: router IP → .env, installs SSH key on device
pnpm dev # Start Vite dev server (proxies to OpenWrt device)
pnpm build # Clean + regenerate tokens + build production assets to htdocs/luci-static/
pnpm gen:tokens # Regenerate src/media/_tokens.css from tokens/ (also runs as part of build)
@ -26,7 +27,7 @@ No linter CLI. Formatting uses Prettier with format-on-save (`.vscode/settings.j
- `.dev/public/aurora/``htdocs/luci-static/aurora/` (copied as-is)
- `ucode/template/themes/aurora/*.ut` — server-side templates, not processed by Vite
**CSS**: two Tailwind CSS v4 entry points in `.dev/src/media/``main.css` (admin UI; a pure import manifest over `_tokens.css`, `_base.css`, `_elements.css`, `_layout.css`, `components/*.css`, `_utilities.css`) and `login.css` (standalone login page; imports `_base.css`). Third-party compatibility patches are not bundled into `main.css`; each lives in `media/patches/<data-page>.css`, builds to its own `htdocs/luci-static/aurora/patches/<data-page>.css`, and is linked on demand by `header.ut`, which discovers installed patches at render time via `fs.lsdir()` and matches them on path-segment-boundary prefixes (dynamic subpages inherit their prefix's patch; third-party packages may drop their own patch into the directory) — see "On-Demand Third-Party Patches" in `.dev/docs/DEVELOPMENT.md`. All styling MUST use TailwindCSS v4 utility classes via `@apply` — no raw CSS properties (e.g. write `@apply text-sm font-semibold;` not `font-size: 14px; font-weight: 600;`). Use [CSS Nesting syntax](https://drafts.csswg.org/css-nesting/) for selectors. Theme colors defined as OKLCH custom properties in `_tokens.css` and mapped via `@theme inline`. `@layer` at-rules stripped by PostCSS plugin for OpenWrt compatibility. See `.dev/docs/DEVELOPMENT.md` for the full CSS file layout.
**CSS**: two Tailwind CSS v4 entry points in `.dev/src/media/``main.css` (admin UI; a pure import manifest over `_tokens.css`, `_base.css`, `_elements.css`, `_layout.css`, `components/*.css`, `_utilities.css`) and `login.css` (standalone login page; imports `_base.css`). Third-party compatibility patches are not bundled into `main.css`; each lives in `media/patches/<data-page>.css`, builds to its own `htdocs/luci-static/aurora/patches/<data-page>.css`, and is linked on demand by `header.ut`, which discovers installed patches at render time via `fs.lsdir()` and matches them on path-segment-boundary prefixes (dynamic subpages inherit their prefix's patch; third-party packages may drop their own patch into the directory) — see "On-Demand Third-Party Patches" in `.dev/docs/DEVELOPMENT.md`. All styling MUST use TailwindCSS v4 utility classes via `@apply` — no raw CSS properties (e.g. write `@apply text-sm font-semibold;` not `font-size: 14px; font-weight: 600;`). Use [CSS Nesting syntax](https://drafts.csswg.org/css-nesting/) for selectors. Theme colors defined as OKLCH custom properties in `_tokens.css` and mapped via `@theme inline`. Built CSS keeps Tailwind's native `@layer` structure (theme partials are unlayered, so they outrank layered utilities); the OKLCH requirement already gates browsers to ones with `@layer` support. See `.dev/docs/DEVELOPMENT.md` for the full CSS file layout.
**Design tokens**: `_tokens.css` is GENERATED — do not hand-edit (see its header comment). Source of truth is `.dev/tokens/`: `defaults.js` holds the 10 editable input colors per mode (`bg`, `surface`, `text`, `brand`, `on_brand`, `link`, `info`, `warning`, `success`, `danger`); `spec.js` declares how every other token derives from those via `mix`/`shade`/`set`/`alpha`/`const` operators plus `FIXED` literals (shadows); `engine.js`/`resolve.js` do the OKLCH math and flatten everything to plain `oklch(...)` values. After changing inputs or derivations, run `pnpm gen:tokens` to rewrite `_tokens.css`, and `pnpm test` to check the color-math and token invariants.
@ -39,5 +40,5 @@ No linter CLI. Formatting uses Prettier with format-on-save (`.vscode/settings.j
## Key References
- **Development guide**: `.dev/docs/DEVELOPMENT.md` — dev server setup, env config, proxy details, CI workflows, directory structure
- **Vite config**: `.dev/vite.config.ts` — custom plugins (luci-js-compress, local-serve, redirect, ut-sync, remove-layers)
- **Vite config**: `.dev/vite.config.ts` — custom plugins (luci-js-compress, local-serve, redirect, ut-sync)
- **Version**: `PKG_VERSION` and `PKG_RELEASE` in `Makefile`

View File

@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=1.0.7
PKG_RELEASE:=42
PKG_VERSION:=1.0.8
PKG_RELEASE:=43
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}[data-page=admin-statistics-graphs] [data-plugin] img:where([data-darkmode=true],[data-darkmode=true] *){--tw-hue-rotate:hue-rotate(150deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,);--tw-invert:invert(100%)}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}[data-page=admin-statistics-graphs] [data-plugin] img:where([data-darkmode=true],[data-darkmode=true] *){--tw-hue-rotate:hue-rotate(150deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,);--tw-invert:invert(100%)}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}

View File

@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid}}[data-page=admin-system-filemanager] #file-manager-container{overflow:auto}[data-page=admin-system-filemanager] #file-manager-container #status-bar{border-style:var(--tw-border-style);background-color:var(--surface);border-width:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid}}}[data-page=admin-system-filemanager] #file-manager-container{overflow:auto}[data-page=admin-system-filemanager] #file-manager-container #status-bar{border-style:var(--tw-border-style);background-color:var(--surface);border-width:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}

View File

@ -2,20 +2,28 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mwan3-ledhelper
PKG_VERSION:=0.0.2
PKG_RELEASE:=2
PKG_RELEASE:=3
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
PKGARCH:=all
define Package/$(PKG_NAME)/Default
SECTION:=net
CATEGORY:=Network
SUBMENU:=WAN
TITLE:=MWAN3 Led Helper
PKGARCH:=all
endef
define Package/$(PKG_NAME)
$(call Package/$(PKG_NAME)/Default)
DEPENDS:=+mwan3
endef
define Package/$(PKG_NAME)-nft
PKGARCH:=all
TITLE:=MWAN3 Led Helper (nft variant)
$(call Package/$(PKG_NAME)/Default)
TITLE+= (nft variant)
DEPENDS:=+mwan3-nft
endef
@ -23,6 +31,10 @@ define Package/$(PKG_NAME)/description
Hotplug script to indicate led for mwan3 link states
endef
define Package/$(PKG_NAME)-nft/description
Hotplug script to indicate led for mwan3 link states (nftables/fw4 variant).
endef
define Build/Prepare
endef
@ -41,6 +53,10 @@ define Package/$(PKG_NAME)-nft/install
$(CP) ./root/* $(1)/
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
$(eval $(call BuildPackage,$(PKG_NAME)-nft))
ifneq ($(BUILD_NFT_ONLY),1)
$(eval $(call BuildPackage,$(PKG_NAME)))
endif
ifneq ($(BUILD_IPT_ONLY),1)
$(eval $(call BuildPackage,$(PKG_NAME)-nft))
endif

View File

@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260531
PKG_RELEASE:=36
PKG_RELEASE:=37
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=756fea6d1e541e3d693dea4f82969585ad9262cc
PKG_SOURCE_VERSION:=5430b3312cd783ba2179a24297979252c0463bb9
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=xmm-modem
PKG_VERSION:=0.1.6
PKG_RELEASE:=1
PKG_VERSION:=0.1.7
PKG_RELEASE:=2
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
include $(INCLUDE_DIR)/package.mk

View File

@ -1,16 +0,0 @@
opengt
set com 115200n81
set comecho off
set senddelay 0.02
waitquiet 0.2 0.2
send "AT+CGAUTH="
send $env("CID")
send ","
send $env("AUTH")
send ",\""
send $env("USER")
send "\",\""
send $env("PASS")
send "\"^m"

View File

@ -1,17 +0,0 @@
opengt
set com 115200n81
set comecho off
set senddelay 0.02
waitquiet 0.2 0.2
send "AT+CGPADDR="
send $env("CID")
send "^m"
get 1 "" $s
print $s
send "AT+GTDNS="
send $env("CID")
send "^m"
get 1 "" $s
print $s

View File

@ -1,28 +0,0 @@
opengt
set com 115200n81
set comecho off
set senddelay 0.02
waitquiet 0.2 0.2
send "AT+CMEE=2^m"
send "AT+COPS=0^m"
send "AT+CGPIAF=1,0,0,0^m"
send "AT+CGDCONT="
send $env("CID")
send "^m"
send "AT+CGDCONT="
send $env("CID")
send ",\""
send $env("PDP")
send "\",\""
send $env("APN")
send "\"^m"
send "AT+CGACT=1,"
send $env("CID")
send "^m"
send "AT+CGPADDR="
send $env("CID")
send "^m"
get 1 "" $s
print $s

View File

@ -4,7 +4,9 @@ opengt
set senddelay 0.02
waitquiet 0.2 0.2
send "AT+XGAUTH="
send "AT+"
send $env("AUTHCMD")
send "="
send $env("CID")
send ","
send $env("AUTH")

View File

@ -10,6 +10,8 @@ opengt
get 1 "" $s
print $s
send "AT+XDNS?^m"
send "AT+"
send $env("DNSQUERY")
send "^m"
get 1 "" $s
print $s

View File

@ -7,9 +7,16 @@ opengt
send "AT+CMEE=2^m"
send "AT+COPS=0^m"
send "AT+CGPIAF=1,0,0,0^m"
if $env("PREFIX") = "xmm" goto sendxdns
goto skipxdns
:sendxdns
send "AT+XDNS="
send $env("CID")
send ",1^m"
:skipxdns
send "AT+CGDCONT="
send $env("CID")
send "^m"
@ -27,11 +34,17 @@ opengt
send $env("CID")
send "^m"
if $env("PREFIX") = "xmm" goto senddatachannel
goto skipdatachannel
:senddatachannel
send "AT+XDATACHANNEL=1,1,\"/USBCDC/0\",\"/USBHS/NCM/0\",2,"
send $env("CID")
send "^m"
send "AT+CGDATA=\"M-RAW_IP\","
send $env("CID")
send "^m"
:skipdatachannel
get 1 "" $s
print $s

View File

@ -7,4 +7,5 @@ opengt
send "AT+CGACT=0,"
send $env("CID")
send "^m"
send "AT+XDATACHANNEL=0^m"

View File

@ -61,11 +61,15 @@ proto_xmm_setup() {
PREFIX="xmm"
hwaddr="$(ls -1 $devpath/../*/net/*/*address*)"
XMMDNS="XDNS"
AUTHCMD="XGAUTH"
DNSQUERY="XDNS?"
;;
0e8d7126|0e8d7127)
PREFIX="fm350"
hwaddr="$(ls -1 $devpath/../../*/net/*/*address*)"
XMMDNS="GTDNS"
AUTHCMD="CGAUTH"
DNSQUERY="GTDNS=$profile"
;;
*)
echo "Modem not supported!"
@ -148,14 +152,14 @@ proto_xmm_setup() {
chap) AUTH=2 ;;
*) AUTH=0 ;;
esac
CID=$profile AUTH=$AUTH USER="$username" PASS="$password" gcom -d "$device" -s /etc/gcom/${PREFIX}-auth.gcom >/dev/null 2>&1
CID=$profile AUTH=$AUTH AUTHCMD=$AUTHCMD USER="$username" PASS="$password" gcom -d "$device" -s /etc/gcom/xmm-auth.gcom >/dev/null 2>&1
}
CID=$profile APN=$apn PDP=$pdp gcom -d $device -s /etc/gcom/${PREFIX}-connect.gcom >/dev/null 2>&1
CID=$profile APN=$apn PDP=$pdp PREFIX=$PREFIX gcom -d $device -s /etc/gcom/xmm-connect.gcom >/dev/null 2>&1
proto_init_update "$ifname" 1
proto_add_data
proto_close_data
DATA=$(CID=$profile gcom -d $device -s /etc/gcom/${PREFIX}-config.gcom)
DATA=$(CID=$profile DNSQUERY=$DNSQUERY gcom -d $device -s /etc/gcom/xmm-config.gcom)
ip4addr=$(echo "$DATA" | awk -F [,] '/^\+CGPADDR/{gsub("\r|\"", ""); print $2}') >/dev/null 2>&1
lladdr=$(echo "$DATA" | awk -F [,] '/^\+CGPADDR/{gsub("\r|\"", ""); print $3}') >/dev/null 2>&1
ns=$(echo "$DATA" | awk -F [,] '/^\+'$XMMDNS': /{gsub("\r|\"",""); gsub("0.0.0.0",""); print $2" "$3}' | sed 's/^[[:space:]]//g' | uniq)