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:
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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user