mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
🌴 Sync 2026-07-07 04:15:13
This commit is contained in:
parent
2a528e3190
commit
5c17703277
@ -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)?
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
luci-theme-aurora/.dev/scripts/setup.js
Normal file
93
luci-theme-aurora/.dev/scripts/setup.js
Normal 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`.");
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
]),
|
||||
),
|
||||
},
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user