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

This commit is contained in:
github-actions[bot] committed 2026-07-07 04:15:13 +08:00
1 parent bda6f7a772
commit add8ea0d42
33 files changed
+446 -401

No files matched your search

+2 -2
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)?
+1 -1
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)
@@ -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) {
@@ -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();
}
});
+1 -1
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:= \
@@ -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) {
@@ -4,7 +4,7 @@
"read": {
"file": {
"/etc/samba/smb.conf.template": [ "read" ],
"/usr/sbin/smbd": [ "exec" ]
"/usr/sbin/smbd -V": [ "exec" ]
},
"uci": [ "samba4" ]
},
@@ -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),
@@ -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),
+8 -13
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
+19 -45
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
+1
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",
+93
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`.");
+84 -86
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;
}
& img {
@apply h-6 w-6;
}
}
.login-title {
@apply flex flex-col gap-1;
}
.login-title {
@apply flex flex-col gap-1;
}
.login-hostname {
@apply text-text text-xl font-semibold tracking-tight;
}
.login-hostname {
@apply text-text text-xl font-semibold tracking-tight;
}
.login-subtitle {
@apply text-text text-sm;
}
.login-subtitle {
@apply text-text text-sm;
}
.login-form {
@apply flex flex-col gap-5;
}
.login-form {
@apply flex flex-col gap-5;
}
.login-fields {
@apply flex flex-col gap-4;
}
.login-fields {
@apply flex flex-col gap-4;
}
.login-field {
@apply flex flex-col gap-2;
}
.login-field {
@apply flex flex-col gap-2;
}
.login-label {
@apply text-text text-xs font-semibold tracking-widest uppercase;
}
.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];
}
.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];
}
/* 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;
}
.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-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-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-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-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;
.login-footer {
@apply text-text-muted absolute right-0 bottom-5 left-0 px-5 text-center text-xs;
& p {
@apply whitespace-normal;
}
& p {
@apply whitespace-normal;
}
& a {
@apply text-brand hover:underline;
}
& a {
@apply text-brand hover:underline;
}
}
+148 -138
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),
]),
),
},
+3 -2
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`
+2 -2
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:=
@@ -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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--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;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--animate-spin:spin 1s linear infinite;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(selecLine truncated
@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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--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;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-tracking:initial}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--animate-spin:spin 1s linear infinite;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;backgrLine truncated
@@ -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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--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;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-tracking:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feLine truncated
@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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--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;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-tracking:initial;--tw-ease:initial;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);fonLine truncated
@@ -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;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--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-dashboard] .Dashboard>.section-content .dashboard-bg.tr{border-radius:0}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg{background-color:var(--surface-overlay)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg:not(.tr){border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);--tw-shadow:var(--app-shadow-md);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg:not(.tr):hover{border-color:var(--hairline);--tw-shadow:var(--app-shadow-lg);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg{background-color:var(--surface-overlay)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg:not(.tr){border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline)}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg span:where([data-darkmode=true],[data-darkmode=true] *),[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg td:where([data-darkmode=true],[data-darkmode=true] *),[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg th:where([data-darkmode=true],[data-darkmode=true] *){color:var(--text)!important}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg img[src*=\.svg]:where([data-darkmode=true],[data-darkmode=true] *){--tw-invert:invert(100%);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,)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .td{padding-block:calc(var(--spacing)*2)}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .td{padding-block:calc(var(--spacing)*1)}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .th{padding-block:calc(var(--spacing)*2)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist){margin-bottom:calc(var(--spacing)*0);border-style:var(--tw-border-style);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#0000;border-width:0;border-radius:0}@media (hover:hover){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) tr:hover{background-color:#0000}}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) tr{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing)*0);padding-block:calc(var(--spacing)*0);border-width:0;display:table-row}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) td{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing)*0);padding-block:calc(var(--spacing)*.5);border-width:0}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) td{display:table-cell}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) td:first-child{padding-right:calc(var(--spacing)*2);white-space:nowrap;opacity:.7}[data-page=admin-dashboard] .Dashboard>.section-content .title h3{border-style:var(--tw-border-style);padding-bottom:calc(var(--spacing)*4);color:var(--text);border-width:0}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .title h3{margin-inline:calc(var(--spacing)*0);padding-bottom:calc(var(--spacing)*2)}}[data-page=admin-dashboard] .Dashboard>.section-content .router-status-wifi .wifi-info .devices-info .tr .td:nth-child(3){top:calc(var(--spacing)*0);width:100%;padding-inline:cLine truncated
@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;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--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-dashboard] .Dashboard>.section-content .dashboard-bg.tr{border-radius:0}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg{background-color:var(--surface-overlay)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg:not(.tr){border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);--tw-shadow:var(--app-shadow-md);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg:not(.tr):hover{border-color:var(--hairline);--tw-shadow:var(--app-shadow-lg);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg{background-color:var(--surface-overlay)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg:not(.tr){border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline)}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg span:where([data-darkmode=true],[data-darkmode=true] *),[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg td:where([data-darkmode=true],[data-darkmode=true] *),[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg th:where([data-darkmode=true],[data-darkmode=true] *){color:var(--text)!important}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg img[src*=\.svg]:where([data-darkmode=true],[data-darkmode=true] *){--tw-invert:invert(100%);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,)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .td{padding-block:calc(var(--spacing)*2)}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .td{padding-block:calc(var(--spacing)*1)}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .th{padding-block:calc(var(--spacing)*2)}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist){margin-bottom:calc(var(--spacing)*0);border-style:var(--tw-border-style);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#0000;border-width:0;border-radius:0}@media (hover:hover){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) tr:hover{background-color:#0000}}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) tr{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing)*0);padding-block:calc(var(--spacing)*0);border-width:0;display:table-row}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) td{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing)*0);padding-block:calc(var(--spacing)*.5);border-width:0}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) td{display:table-cell}}[data-page=admin-dashboard] .Dashboard>.section-content .dashboard-bg .table:not(.assoclist) td:first-child{padding-right:calc(var(--spacing)*2);white-space:nowrap;opacity:.7}[data-page=admin-dashboard] .Dashboard>.section-content .title h3{border-style:var(--tw-border-style);padding-bottom:calc(var(--spacing)*4);color:var(--text);border-width:0}@media not all and (min-width:48rem){[data-page=admin-dashboard] .Dashboard>.section-content .title h3{margin-inline:calc(var(--spacing)*0);padding-bottom:calc(var(--spacing)*2)}}[data-page=admin-dashboard] .Dashboard>.section-content .router-status-wifi .wifi-info .devices-info .tr .td:nth-child(3){top:calc(var(--spacing)*0);width:10Line truncated
@@ -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}
@@ -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}
+23 -7
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
+2 -2
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
+2 -2
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
-16
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"
-17
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
@@ -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
+3 -1
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")
+3 -1
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
+13
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
@@ -7,4 +7,5 @@ opengt
send "AT+CGACT=0,"
send $env("CID")
send "^m"
send "AT+XDATACHANNEL=0^m"
+7 -3
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)