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
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/);
});
});