feat(security,ci,ui): harden CI/CD, LuCI ACL, low-space updates and mobile layout

- ci: make frontend CI read-only with strict diff check and deterministic calls.json
- ci: pin external GitHub Actions in workflows to immutable commit SHAs
- ci: add openwrt-e2e blocking gate job running docker_e2e_test.sh
- fix(diagnostic): fix contstants.ts typo and update all 13 import sites
- fix(acl): remove unused /bin/busybox exec from rpcd ACL and add regression test
- feat(updates): add low-space update fallback when creating sing-box.old is impossible
- fix(routes): preserve dedicated Discord gateway subnets (162.159.128.0/21)
- feat(ui): implement responsive auto-fit grid and mobile modal viewport bounds
- test: add viewport regression test suite in vitest
This commit is contained in:
Dushnilin
2026-09-14 23:00:18 +03:00
parent 7375e26b44
commit 603c0df91b
41 changed files with 5420 additions and 5198 deletions
+16 -1
View File
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@@ -71,3 +71,18 @@ jobs:
! -name 'ucode_syntax_lint.sh' \
-print0 | xargs -0 -n1 -P4 bash
openwrt-e2e:
name: OpenWrt Docker E2E
needs: backend-checks
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Run OpenWrt E2E suite
shell: bash
run: bash tests/docker_e2e_test.sh
+7 -7
View File
@@ -25,7 +25,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
commitish: ${{ steps.version.outputs.commitish }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- id: version
@@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-latest
needs: preparation
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@@ -63,7 +63,7 @@ jobs:
ls -la "$PWD/filtered-bin/release"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: release-files-${{ needs.preparation.outputs.version }}
path: ./filtered-bin/release/*
@@ -77,12 +77,12 @@ jobs:
- preparation
- build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Create release dir
run: mkdir -p ./filtered-bin/release
- name: Download release artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: release-files-${{ needs.preparation.outputs.version }}
path: ./filtered-bin/release
@@ -118,7 +118,7 @@ jobs:
- name: Release on tag push
if: ${{ github.event_name == 'push' }}
uses: softprops/action-gh-release@v2.4.0
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
files: ./filtered-bin/release/*
tag_name: ${{ github.ref_name }}
@@ -133,7 +133,7 @@ jobs:
- name: Release on workflow dispatch
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: softprops/action-gh-release@v2.4.0
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
files: ./filtered-bin/release/*
tag_name: ${{ needs.preparation.outputs.version }}
+33 -9
View File
@@ -7,6 +7,14 @@ on:
pull_request:
paths:
- 'fe-app-tachyon/**'
- 'luci-app-tachyon/po/**'
- '.github/workflows/frontend-ci.yml'
push:
branches:
- main
paths:
- 'fe-app-tachyon/**'
- 'luci-app-tachyon/po/**'
- '.github/workflows/frontend-ci.yml'
jobs:
@@ -19,17 +27,15 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v5.0.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22'
cache: npm
cache-dependency-path: fe-app-tachyon/package-lock.json
# The project uses npm (package-lock.json); yarn.lock does not exist, so
# a yarn install here would resolve fresh versions on every run.
- name: Install dependencies
run: npm ci
@@ -48,6 +54,24 @@ jobs:
- name: Run tests
run: npm test -- --run
- name: Check translation extraction
id: locale_extract
run: |
npm run locales:extract-calls
if ! git diff --exit-code -- locales/calls.json; then
echo "::error::Translation call index is stale. Run 'npm run locales:extract-calls' locally."
exit 1
fi
- name: Check distributed locales
id: locale_distribute
run: |
npm run locales:distribute
if ! git diff --exit-code -- ../luci-app-tachyon/po; then
echo "::error::LuCI locale copies are stale. Run 'npm run locales:distribute' locally."
exit 1
fi
- name: Build project
id: build
run: |
@@ -62,8 +86,8 @@ jobs:
run: |
echo "## Frontend CI Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Format check: ${{ steps.format.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Lint check: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Tests: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Build: ${{ steps.build.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "![Success](https://cdn2.combot.org/boratbrat/webp/6xf09f988f.webp)" >> $GITHUB_STEP_SUMMARY
echo "- Format check: ${{ steps.format.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "- Translation extraction: ${{ steps.locale_extract.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "- Locale distribution: ${{ steps.locale_distribute.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "- Overall lint/tests status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- Build: ${{ steps.build.outcome }}" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install ShellCheck
run: |
+2
View File
@@ -1,6 +1,8 @@
fe-app-tachyon/node_modules
fe-app-tachyon/.yarn/
fe-app-tachyon/.env
fe-app-tachyon/.vitest/
fe-app-tachyon/coverage/
.DS_Store
shellcheck-v0.10.0/
AGENTS.md
File diff suppressed because one or more lines are too long
+10 -4
View File
@@ -17,7 +17,7 @@ function stripIllegalReturn(code) {
});
}
const files = await glob([
const rawFiles = await glob([
'src/**/*.ts',
'src/**/*.tsx',
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/**/*.js',
@@ -29,6 +29,7 @@ const files = await glob([
],
absolute: true,
});
const files = rawFiles.sort();
console.log('Found files:', files.length);
const results = {};
@@ -36,7 +37,7 @@ const results = {};
for (const file of files) {
const contentRaw = await fs.readFile(file, 'utf8');
const content = stripIllegalReturn(contentRaw);
const relativePath = path.relative(process.cwd(), file).replaceAll('\\', '/');
const relativePath = path.relative(__dirname, file).replaceAll('\\', '/');
let ast;
try {
@@ -73,8 +74,13 @@ for (const file of files) {
});
}
const outFile = 'locales/calls.json';
const sorted = Object.values(results).sort((a, b) => a.key.localeCompare(b.key)); // 🔤 сортировка по ключу
const outFile = path.resolve(__dirname, 'locales/calls.json');
const sorted = Object.values(results)
.map((item) => ({
...item,
places: Array.from(new Set(item.places)).sort(),
}))
.sort((a, b) => a.key.localeCompare(b.key)); // 🔤 сортировка по ключу
await fs.mkdir(path.dirname(outFile), { recursive: true });
await fs.writeFile(outFile, JSON.stringify(sorted, null, 2), 'utf8');
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-14 14:18+0300\n"
"PO-Revision-Date: 2026-09-14 14:18+0300\n"
"POT-Creation-Date: 2026-09-14 22:41+0300\n"
"PO-Revision-Date: 2026-09-14 22:41+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
+8 -6
View File
@@ -1,17 +1,13 @@
{
"name": "fe-app-tachyon",
"version": "1.3.24",
"version": "1.3.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fe-app-tachyon",
"version": "1.3.24",
"version": "1.3.26",
"license": "MIT",
"dependencies": {
"@esbuild/win32-x64": "^0.28.2",
"@rolldown/binding-win32-x64-msvc": "^1.2.6"
},
"devDependencies": {
"@babel/parser": "7.28.4",
"@babel/traverse": "7.28.4",
@@ -29,6 +25,10 @@
"typescript": "5.9.3",
"typescript-eslint": "8.45.0",
"vitest": "^5.0.0"
},
"optionalDependencies": {
"@esbuild/win32-x64": "^0.28.2",
"@rolldown/binding-win32-x64-msvc": "^1.2.6"
}
},
"node_modules/@babel/code-frame": {
@@ -615,6 +615,7 @@
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
@@ -1206,6 +1207,7 @@
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
+1 -1
View File
@@ -42,7 +42,7 @@
"postcss": "8.5.23",
"js-yaml": "^5.2.1"
},
"dependencies": {
"optionalDependencies": {
"@esbuild/win32-x64": "^0.28.2",
"@rolldown/binding-win32-x64-msvc": "^1.2.6"
}
+21 -12
View File
@@ -164,9 +164,10 @@ ${PartialStyles}
transform: translateY(0);
}
/* Global Mobile & Responsive Layout Enhancements */
/* Tachyon Mobile & Responsive Layout Enhancements */
@media (max-width: 768px) {
.cbi-section-table {
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-section-table,
.tachyon-view .cbi-section-table {
display: block;
width: 100%;
overflow-x: auto;
@@ -174,33 +175,41 @@ ${PartialStyles}
border-collapse: collapse;
}
.cbi-section-table-cell {
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-section-table-cell,
.tachyon-view .cbi-section-table-cell {
min-width: 0;
word-break: break-word;
overflow-wrap: anywhere;
}
.cbi-section-actions {
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-section-actions,
.tachyon-view .cbi-section-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cbi-dropdown {
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-dropdown,
.tachyon-view .cbi-dropdown {
max-width: 100%;
}
.cbi-input-textarea,
.cbi-input-text,
.cbi-input-select {
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-input-textarea,
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-input-text,
[id^="cbi-${TACHYON_CBI_PREFIX}"] .cbi-input-select,
.tachyon-view .cbi-input-textarea,
.tachyon-view .cbi-input-text,
.tachyon-view .cbi-input-select {
max-width: 100%;
box-sizing: border-box;
}
/* Modal responsiveness */
.modal,
.cbi-modal {
max-width: 96vw !important;
/* Modal responsiveness scoped to Tachyon */
.modal .fkp-server-info-modal,
.cbi-modal .fkp-server-info-modal,
.modal:has([class*="tachyon"]),
.cbi-modal:has([class*="tachyon"]) {
max-width: min(96vw, 1100px) !important;
box-sizing: border-box;
margin: 10px auto;
}
@@ -29,9 +29,9 @@ export const styles = `
}
.tachyon_dashboard-page {
display: table;
table-layout: fixed;
display: block;
width: 100%;
box-sizing: border-box;
--dashboard-grid-columns: 4;
--dashboard-grid-min-width: 180px;
}
@@ -144,11 +144,13 @@ export const styles = `
align-items: center;
justify-content: flex-end;
gap: 6px;
flex: 0 0 auto;
flex: 0 1 auto;
flex-wrap: wrap;
}
.tachyon_dashboard-page .btn.tachyon_dashboard-page__outbound-section__subscription-update {
min-width: 130px;
min-width: 0;
width: auto;
min-height: 28px;
display: inline-flex;
align-items: center;
@@ -169,7 +171,8 @@ export const styles = `
}
.tachyon_dashboard-page .btn.dashboard-sections-grid-item-test-latency {
min-width: 99px;
min-width: 0;
width: auto;
min-height: 28px;
display: inline-flex;
align-items: center;
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { IDiagnosticsChecksItem } from '../../../services';
@@ -1,5 +1,5 @@
import { insertIf } from '../../../../helpers';
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import type { IDiagnosticsChecksItem } from '../../../services';
import { updateCheckStore } from './updateCheckStore';
@@ -1,5 +1,5 @@
import { insertIf } from '../../../../helpers';
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods, RemoteFakeIPMethods } from '../../../methods';
import { getDashboardSections } from '../../../methods/custom/getDashboardSections';
import { store, type IDiagnosticsChecksItem } from '../../../services';
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { RemoteFakeIPMethods, TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { IDiagnosticsChecksItem } from '../../../services';
@@ -1,4 +1,4 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { DIAGNOSTICS_CHECKS_MAP } from './constants';
import { TachyonShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { IDiagnosticsChecksItem } from '../../../services';
@@ -1,7 +1,4 @@
import {
DIAGNOSTICS_CHECKS,
DIAGNOSTICS_CHECKS_MAP,
} from './checks/contstants';
import { DIAGNOSTICS_CHECKS, DIAGNOSTICS_CHECKS_MAP } from './checks/constants';
import { IDiagnosticsChecksStoreItem, StoreType } from '../../services';
export interface DiagnosticsProviderOptions {
@@ -1,6 +1,6 @@
import type { StoreType } from '../../services';
import type { DiagnosticsProviderOptions } from './diagnostic.store';
import { DIAGNOSTICS_CHECKS } from './checks/contstants';
import { DIAGNOSTICS_CHECKS } from './checks/constants';
const DIAGNOSTIC_RUN_STORAGE_KEY = 'tachyon:diagnostic-run:v1';
const DIAGNOSTIC_RUN_TTL_MS = 30 * 60 * 1000;
@@ -14,10 +14,7 @@ import { runFakeIPCheck } from './checks/runFakeIPCheck';
import { runZapretCheck } from './checks/runZapretCheck';
import { runZapret2Check } from './checks/runZapret2Check';
import { runByedpiCheck } from './checks/runByedpiCheck';
import {
DIAGNOSTICS_CHECKS,
DIAGNOSTICS_CHECKS_MAP,
} from './checks/contstants';
import { DIAGNOSTICS_CHECKS, DIAGNOSTICS_CHECKS_MAP } from './checks/constants';
import {
DiagnosticsProviderOptions,
getDiagnosticsChecks,
@@ -137,7 +137,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
const controlsGrid = E('div', {
style:
'display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; align-items: end;',
'display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr)); gap: 10px; align-items: end;',
});
// 1. Engine Select
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { DIAGNOSTICS_CHECKS } from '../checks/contstants';
import { DIAGNOSTICS_CHECKS } from '../checks/constants';
import { getDiagnosticsChecks } from '../diagnostic.store';
describe('diagnostic store provider checks', () => {
@@ -0,0 +1,76 @@
import { describe, it, expect } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
class MockMutationObserver {
observe() {}
disconnect() {}
takeRecords() {
return [];
}
}
globalThis.MutationObserver = MockMutationObserver;
globalThis.document = {
body: {},
createElement: () => ({}),
getElementById: () => null,
querySelector: () => null,
querySelectorAll: () => [],
};
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
describe('Viewport and Mobile Responsiveness Regressions', async () => {
const { styles: dashboardStyles } = await import('../tabs/dashboard/styles');
const { GlobalStyles } = await import('../../styles');
it('does not use display: table or table-layout: fixed on the dashboard root', () => {
expect(dashboardStyles).not.toMatch(
/\.tachyon_dashboard-page\s*\{[^}]*display:\s*table/,
);
expect(dashboardStyles).not.toMatch(/table-layout:\s*fixed/);
expect(dashboardStyles).toMatch(
/\.tachyon_dashboard-page\s*\{[^}]*display:\s*block/,
);
});
it('removes rigid desktop min-width limits from dashboard buttons', () => {
expect(dashboardStyles).not.toMatch(
/\.tachyon_dashboard-page\s+\.btn\.tachyon_dashboard-page__outbound-section__subscription-update\s*\{[^}]*min-width:\s*130px/,
);
expect(dashboardStyles).not.toMatch(
/\.tachyon_dashboard-page\s+\.btn\.dashboard-sections-grid-item-test-latency\s*\{[^}]*min-width:\s*99px/,
);
expect(dashboardStyles).toMatch(
/\.tachyon_dashboard-page__outbound-section__title-section__actions\s*\{[^}]*flex-wrap:\s*wrap/,
);
});
it('ensures fkp-server-info-modal is fluid and responsive on mobile screens', () => {
const serverJsPath = path.resolve(
__dirname,
'../../../../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/server.js',
);
const serverJsContent = fs.readFileSync(serverJsPath, 'utf8');
expect(serverJsContent).toMatch(
/\.fkp-server-info-modal\{[^}]*width:\s*min\(560px,\s*calc\(100vw\s*-\s*32px\)\)/,
);
expect(serverJsContent).toMatch(
/\.fkp-server-info-modal__qr-image\{[^}]*max-width:\s*min\(180px,\s*60vw\)/,
);
expect(serverJsContent).toMatch(
/@media\s*\(max-width:\s*640px\)\{\s*\.fkp-server-info-modal\{width:100%\}/,
);
});
it('scopes responsive CBI table rules to Tachyon selectors', () => {
expect(GlobalStyles).not.toMatch(
/@media\s*\(max-width:\s*768px\)\s*\{\s*\.cbi-section-table\s*\{/,
);
expect(GlobalStyles).toMatch(/\[id\^="cbi-tachyon"\] \.cbi-section-table/);
});
});
@@ -5859,7 +5859,7 @@ function getCheckTitle(name) {
return `${name} ${_("checks")}`;
}
// src/tachyon/tabs/diagnostic/checks/contstants.ts
// src/tachyon/tabs/diagnostic/checks/constants.ts
var DIAGNOSTICS_CHECKS = /* @__PURE__ */ ((DIAGNOSTICS_CHECKS2) => {
DIAGNOSTICS_CHECKS2["DNS"] = "DNS";
DIAGNOSTICS_CHECKS2["SINGBOX"] = "SINGBOX";
@@ -9411,9 +9411,9 @@ var styles = `
}
.tachyon_dashboard-page {
display: table;
table-layout: fixed;
display: block;
width: 100%;
box-sizing: border-box;
--dashboard-grid-columns: 4;
--dashboard-grid-min-width: 180px;
}
@@ -9526,11 +9526,13 @@ var styles = `
align-items: center;
justify-content: flex-end;
gap: 6px;
flex: 0 0 auto;
flex: 0 1 auto;
flex-wrap: wrap;
}
.tachyon_dashboard-page .btn.tachyon_dashboard-page__outbound-section__subscription-update {
min-width: 130px;
min-width: 0;
width: auto;
min-height: 28px;
display: inline-flex;
align-items: center;
@@ -9551,7 +9553,8 @@ var styles = `
}
.tachyon_dashboard-page .btn.dashboard-sections-grid-item-test-latency {
min-width: 99px;
min-width: 0;
width: auto;
min-height: 28px;
display: inline-flex;
align-items: center;
@@ -13041,7 +13044,7 @@ function renderStrategyFuzzerModal(ruleNames = []) {
if (activeTab2 === "history") renderHistoryTab();
};
const controlsGrid = E("div", {
style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; align-items: end;"
style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr)); gap: 10px; align-items: end;"
});
const engineSelect = E(
"select",
@@ -23092,9 +23095,10 @@ ${PartialStyles}
transform: translateY(0);
}
/* Global Mobile & Responsive Layout Enhancements */
/* Tachyon Mobile & Responsive Layout Enhancements */
@media (max-width: 768px) {
.cbi-section-table {
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-section-table,
.tachyon-view .cbi-section-table {
display: block;
width: 100%;
overflow-x: auto;
@@ -23102,33 +23106,41 @@ ${PartialStyles}
border-collapse: collapse;
}
.cbi-section-table-cell {
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-section-table-cell,
.tachyon-view .cbi-section-table-cell {
min-width: 0;
word-break: break-word;
overflow-wrap: anywhere;
}
.cbi-section-actions {
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-section-actions,
.tachyon-view .cbi-section-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cbi-dropdown {
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-dropdown,
.tachyon-view .cbi-dropdown {
max-width: 100%;
}
.cbi-input-textarea,
.cbi-input-text,
.cbi-input-select {
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-input-textarea,
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-input-text,
[id^="cbi-${TACHYON_UCI_PACKAGE}"] .cbi-input-select,
.tachyon-view .cbi-input-textarea,
.tachyon-view .cbi-input-text,
.tachyon-view .cbi-input-select {
max-width: 100%;
box-sizing: border-box;
}
/* Modal responsiveness */
.modal,
.cbi-modal {
max-width: 96vw !important;
/* Modal responsiveness scoped to Tachyon */
.modal .fkp-server-info-modal,
.cbi-modal .fkp-server-info-modal,
.modal:has([class*="tachyon"]),
.cbi-modal:has([class*="tachyon"]) {
max-width: min(96vw, 1100px) !important;
box-sizing: border-box;
margin: 10px auto;
}
@@ -468,10 +468,10 @@ function injectServerStyles() {
#cbi-${UCI_PACKAGE}-server .cbi-section-actions > div{display:inline-flex;align-items:center;gap:4px}
#cbi-${UCI_PACKAGE}-server .cbi-section-actions{text-align:right}
.fkp-server-info-modal{display:flex;flex-direction:column;gap:14px;width:560px;max-width:100%;box-sizing:border-box}
.fkp-server-info-modal{display:flex;flex-direction:column;gap:14px;width:min(560px,calc(100vw - 32px));max-width:100%;box-sizing:border-box}
.fkp-server-info-modal__container{display:flex;gap:24px;align-items:flex-start;width:100%;box-sizing:border-box}
.fkp-server-info-modal__qr-col{display:flex;flex-direction:column;align-items:center;gap:12px;flex-shrink:0;width:180px}
.fkp-server-info-modal__qr-image{width:180px;height:180px;display:block;image-rendering:pixelated}
.fkp-server-info-modal__qr-image{width:180px;height:180px;max-width:min(180px,60vw);max-height:min(180px,60vw);display:block;image-rendering:pixelated}
.fkp-server-info-modal__details-col{display:flex;flex-direction:column;gap:16px;flex-grow:1;min-width:0;box-sizing:border-box}
.fkp-server-info-modal__details-grid{display:flex;flex-direction:column;gap:10px;background:rgba(128,128,128,0.08);padding:14px;border:1px solid rgba(128,128,128,0.15);box-sizing:border-box}
.fkp-server-info-modal__detail-item{display:flex;justify-content:space-between;align-items:center;gap:16px}
@@ -486,9 +486,11 @@ function injectServerStyles() {
@media (max-width:640px){
.fkp-server-info-modal{width:100%}
.fkp-server-info-modal__container{flex-direction:column;align-items:center;gap:20px}
.fkp-server-info-modal__qr-col{width:100%;max-width:180px}
.fkp-server-info-modal__details-col{width:100%}
.fkp-server-info-modal__container{flex-direction:column;align-items:center;gap:16px}
.fkp-server-info-modal__qr-col{width:100%;max-width:180px;align-items:center}
.fkp-server-info-modal__details-col{width:100%;min-width:0}
.fkp-server-info-modal__details-grid{padding:10px}
.fkp-server-info-modal__detail-item{flex-wrap:wrap;gap:4px 8px}
}
`,
),
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-14 14:18+0300\n"
"PO-Revision-Date: 2026-09-14 14:18+0300\n"
"POT-Creation-Date: 2026-09-14 22:41+0300\n"
"PO-Revision-Date: 2026-09-14 22:41+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
File diff suppressed because it is too large Load Diff
@@ -62,9 +62,6 @@
],
"/usr/bin/nslookup": [
"exec"
],
"/bin/busybox": [
"exec"
]
},
"ubus": {
+37 -6
View File
@@ -1825,6 +1825,31 @@ function move_file_to_backup(target_path, backup_path) {
return move_file_portable(target_path, backup_path);
}
// Binary variants are fully extracted and validated before this helper is
// called. On storage-constrained routers the temporary rollback copy can
// be larger than the remaining /tmp space. If persistent component
// backups are disabled, it is safer to free the old binary and install
// the already validated replacement than to fail the update solely
// because a second copy cannot be kept in RAM.
function move_validated_file_to_backup_or_discard(target_path, backup_path, label) {
if (!file_exists(target_path))
return "";
if (move_file_to_backup(target_path, backup_path))
return backup_path;
remove_file(backup_path);
if (get_component_backup_enabled())
return null;
updates_log(
"Temporary rollback copy of " + as_string(label) +
" could not be created; component backups are disabled, removing the current file to free space for the validated replacement",
"warn"
);
remove_file(target_path);
return file_exists(target_path) ? null : "";
}
function restore_sing_box_backup(backup_binary) {
if (as_string(backup_binary) != "" && file_nonempty(backup_binary)) {
if (!move_file_portable(backup_binary, "/usr/bin/sing-box"))
@@ -2400,9 +2425,12 @@ function install_sing_box_extended(action, compressed, target_tag) {
let backup_cronet = "";
let cronet_touched = false;
if (file_exists("/usr/bin/sing-box")) {
backup_binary = tmp_dir + "/sing-box.tachyon-backup";
if (!move_file_to_backup("/usr/bin/sing-box", backup_binary)) {
remove_file(backup_binary);
backup_binary = move_validated_file_to_backup_or_discard(
"/usr/bin/sing-box",
tmp_dir + "/sing-box.tachyon-backup",
"current sing-box binary"
);
if (backup_binary == null) {
remove_file(tmp_binary);
remove_file(tmp_cronet);
remove_file(archive_file);
@@ -2562,9 +2590,12 @@ function install_sing_box_lx(action, target_tag) {
let backup_cronet = "";
let cronet_touched = false;
if (file_exists("/usr/bin/sing-box")) {
backup_binary = tmp_dir + "/sing-box.tachyon-backup";
if (!move_file_to_backup("/usr/bin/sing-box", backup_binary)) {
remove_file(backup_binary);
backup_binary = move_validated_file_to_backup_or_discard(
"/usr/bin/sing-box",
tmp_dir + "/sing-box.tachyon-backup",
"current sing-box binary"
);
if (backup_binary == null) {
remove_file(tmp_binary);
remove_file(tmp_cronet);
remove_file(archive_file);
+3 -1
View File
@@ -275,6 +275,7 @@ function is_cloudflare_shared_cidr(value) {
const DISCORD_VOICE_PORTS_NFT = "5000-5020, 3478, 19302, 50000-65535";
const DISCORD_VOICE_PORT_RANGES = [ "5000:5020", "3478:3478", "19302:19302", "50000:65535" ];
const DEFAULT_DISCORD_VOICE_SUBNETS = [ "162.158.0.0/15", "172.64.0.0/13", "2606:4700::/32" ];
const DISCORD_DEDICATED_SUBNETS = [ "162.159.128.0/21" ];
return {
valid_ipv4,
@@ -295,6 +296,7 @@ return {
is_cloudflare_shared_cidr,
DISCORD_VOICE_PORTS_NFT,
DISCORD_VOICE_PORT_RANGES,
DEFAULT_DISCORD_VOICE_SUBNETS
DEFAULT_DISCORD_VOICE_SUBNETS,
DISCORD_DEDICATED_SUBNETS
};
+10 -2
View File
@@ -2044,8 +2044,12 @@ function nft_add_csv_chunks_to_family_sets(csv, table, ipv4_set, ipv6_set, kind,
function nft_community_subnet_lines(path, service, filter_mode) {
let data = fs.readfile(path);
if (data == null) {
if (as_string(service) == "discord" && filter_mode == "only_cloudflare")
if (as_string(service) == "discord") {
if (filter_mode == "only_cloudflare")
return core_ip.DEFAULT_DISCORD_VOICE_SUBNETS || [ "162.158.0.0/15", "172.64.0.0/13", "2606:4700::/32" ];
if (filter_mode == "exclude_cloudflare" || filter_mode == null)
return core_ip.DISCORD_DEDICATED_SUBNETS || [ "162.159.128.0/21" ];
}
return [];
}
@@ -2067,8 +2071,12 @@ function nft_community_subnet_lines(path, service, filter_mode) {
}
}
if (as_string(service) == "discord" && filter_mode == "only_cloudflare" && length(result) == 0)
if (as_string(service) == "discord") {
if (filter_mode == "only_cloudflare" && length(result) == 0)
return core_ip.DEFAULT_DISCORD_VOICE_SUBNETS || [ "162.158.0.0/15", "172.64.0.0/13", "2606:4700::/32" ];
if ((filter_mode == "exclude_cloudflare" || filter_mode == null) && length(result) == 0)
return core_ip.DISCORD_DEDICATED_SUBNETS || [ "162.159.128.0/21" ];
}
return result;
}
@@ -1511,8 +1511,12 @@ function load_community_subnet_cidrs(community, filter_mode) {
}
}
if (service == "discord" && filter_mode == "only_cloudflare" && length(cidrs) == 0)
if (service == "discord") {
if (filter_mode == "only_cloudflare" && length(cidrs) == 0)
return core_ip.DEFAULT_DISCORD_VOICE_SUBNETS || [ "162.158.0.0/15", "172.64.0.0/13", "2606:4700::/32" ];
if ((filter_mode == "exclude_cloudflare" || filter_mode == null) && length(cidrs) == 0)
return core_ip.DISCORD_DEDICATED_SUBNETS || [ "162.159.128.0/21" ];
}
return [];
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ACL="$ROOT_DIR/luci-app-tachyon/root/usr/share/rpcd/acl.d/luci-app-tachyon.json"
fail() {
echo "FAIL: $*" >&2
exit 1
}
[ -f "$ACL" ] || fail "LuCI ACL file is missing"
# BusyBox is a multi-call binary (sh, wget, nc, rm, dd, ...). Granting LuCI
# generic exec access to it effectively broadens the web UI capability far
# beyond the explicit commands Tachyon needs. Keep this regression guard next
# to the ACL so the capability cannot be reintroduced accidentally.
if grep -Fq '"/bin/busybox"' "$ACL"; then
fail "LuCI ACL must not grant generic /bin/busybox exec access"
fi
# The file is JSON consumed by rpcd; catch accidental syntax damage without
# depending on jq, which is not guaranteed to exist in OpenWrt test images.
node -e 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))' "$ACL"
echo "OK: LuCI ACL keeps generic BusyBox execution disabled"
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ACTION_UC="$ROOT_DIR/tachyon/files/usr/lib/components/action.uc"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
grep -Fq 'function move_validated_file_to_backup_or_discard(target_path, backup_path, label)' "$ACTION_UC" ||
fail "validated low-space backup fallback helper is missing"
grep -Fq 'if (get_component_backup_enabled())' "$ACTION_UC" ||
fail "low-space fallback must remain disabled when persistent component backups are requested"
grep -Fq 'component backups are disabled, removing the current file to free space for the validated replacement' "$ACTION_UC" ||
fail "low-space fallback must leave an explicit warning in the update log"
usage_count="$(grep -Fc 'backup_binary = move_validated_file_to_backup_or_discard(' "$ACTION_UC")"
[ "$usage_count" -eq 2 ] ||
fail "validated low-space fallback must be used by compressed extended and lx installs"
assert_validated_before_fallback() {
local start="$1"
local end="$2"
local block validate_line fallback_line
block="$(awk -v start="$start" -v end="$end" '
index($0, start) { inside=1 }
inside && index($0, end) { exit }
inside { print }
' "$ACTION_UC")"
validate_line="$(printf '%s\n' "$block" | grep -n -m1 'validate_sing_box_extended_binary(tmp_binary, tmp_dir)' | cut -d: -f1 || true)"
fallback_line="$(printf '%s\n' "$block" | grep -n -m1 'backup_binary = move_validated_file_to_backup_or_discard(' | cut -d: -f1 || true)"
[ -n "$validate_line" ] || fail "$start does not validate the downloaded binary"
[ -n "$fallback_line" ] || fail "$start does not use the low-space fallback"
[ "$validate_line" -lt "$fallback_line" ] ||
fail "$start must validate the replacement before discarding the current binary"
}
assert_validated_before_fallback 'function install_sing_box_extended(action, compressed, target_tag)' 'function install_sing_box_lx(action, target_tag)'
assert_validated_before_fallback 'function install_sing_box_lx(action, target_tag)' 'function install_package_sing_box(action, tiny)'
printf 'sing-box low-space update policy tests passed\n'