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:
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 "" >> $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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
+1714
-1725
File diff suppressed because it is too large
Load Diff
+1670
-1681
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
Generated
+8
-6
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
`,
|
||||
),
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1670
-1681
File diff suppressed because it is too large
Load Diff
@@ -62,9 +62,6 @@
|
||||
],
|
||||
"/usr/bin/nslookup": [
|
||||
"exec"
|
||||
],
|
||||
"/bin/busybox": [
|
||||
"exec"
|
||||
]
|
||||
},
|
||||
"ubus": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user