Compare commits
8 Commits
948f15d44a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fe92ccb734 | |||
| e0bc67b146 | |||
| 2f7ef55856 | |||
| 5af5042575 | |||
| 3067b42de4 | |||
| 0dd4869cbb | |||
| b8e1e925b3 | |||
| 24f3e5f454 |
@@ -14,8 +14,10 @@
|
||||
# Триггер — тег вида v1.2.3 (совпадает с полем "version" в tauri.conf.json).
|
||||
#
|
||||
# Нужные секреты в Gitea: TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) — как и раньше,
|
||||
# плюс GITEA_SERVER_URL (напр. https://git.example.com, без слэша на конце) и
|
||||
# GITEA_TOKEN (личный токен с правом записи в этот репозиторий).
|
||||
# плюс GITEA_TOKEN (личный токен с правом записи в этот репозиторий).
|
||||
# GITEA_SERVER_URL (напр. https://git.example.com, без слэша на конце) — это
|
||||
# НЕ секрет (публичный URL сервера), задаётся как repo Variable (vars.*), не
|
||||
# Actions secret.
|
||||
#
|
||||
# shell: bash на windows-latest требует Git for Windows/bash в PATH раннера —
|
||||
# на GitHub-хостед раннерах это есть из коробки, на самохостед Gitea-раннере
|
||||
@@ -121,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Publish release to Gitea
|
||||
env:
|
||||
GITEA_SERVER_URL: ${{ secrets.GITEA_SERVER_URL }}
|
||||
GITEA_SERVER_URL: ${{ vars.GITEA_SERVER_URL }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_REPO: ${{ github.repository }}
|
||||
RELEASE_VERSION: ${{ github.ref_name }}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Release Desktop
|
||||
|
||||
# Только десктоп (Windows/macOS/Linux) — Android/iOS обновляются через сторы,
|
||||
# самообновление там запрещено политиками платформ (см. src-tauri/src/lib.rs).
|
||||
# Триггер — тег вида v1.2.3 (совпадает с полем "version" в tauri.conf.json).
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: "macos-latest"
|
||||
args: "--target aarch64-apple-darwin"
|
||||
- platform: "macos-latest"
|
||||
args: "--target x86_64-apple-darwin"
|
||||
- platform: "ubuntu-24.04"
|
||||
args: ""
|
||||
- platform: "windows-latest"
|
||||
args: ""
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Install Linux system deps (webkit2gtk, etc.)
|
||||
if: matrix.platform == 'ubuntu-24.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Приватный ключ для подписи апдейтов — БЕЗ него downloadAndInstall()
|
||||
# на клиентах будет отклонять пакет (сигнатура не совпадёт).
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Netrunner VPN ${{ github.ref_name }}"
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
args: ${{ matrix.args }}
|
||||
@@ -20,7 +20,9 @@
|
||||
"@hugeicons/react": "^1.1.5",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.35.2",
|
||||
|
||||
Generated
+20
@@ -26,9 +26,15 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.0.0
|
||||
version: 2.10.1
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: ^2.0.0
|
||||
version: 2.3.5
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: ^2.10.1
|
||||
version: 2.10.1
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -1584,9 +1590,15 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-process@2.3.1':
|
||||
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
|
||||
|
||||
'@tauri-apps/plugin-shell@2.3.5':
|
||||
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
||||
|
||||
'@tauri-apps/plugin-updater@2.10.1':
|
||||
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
||||
|
||||
'@ts-morph/common@0.27.0':
|
||||
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
|
||||
|
||||
@@ -4518,10 +4530,18 @@ snapshots:
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-process@2.3.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-shell@2.3.5':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-updater@2.10.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@ts-morph/common@0.27.0':
|
||||
dependencies:
|
||||
fast-glob: 3.3.3
|
||||
|
||||
Generated
+143
@@ -70,6 +70,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
@@ -900,6 +909,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -1183,6 +1203,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -2567,6 +2597,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2648,9 +2684,12 @@ dependencies = [
|
||||
"ndk-context",
|
||||
"netrunner-logger",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-vpn",
|
||||
"winres",
|
||||
]
|
||||
@@ -2871,6 +2910,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -2886,6 +2926,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2973,6 +3025,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3803,6 +3869,7 @@ checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
@@ -4620,6 +4687,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4757,6 +4835,16 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
|
||||
dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.3.5"
|
||||
@@ -4778,6 +4866,39 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-vpn"
|
||||
version = "0.1.0"
|
||||
@@ -6673,6 +6794,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
@@ -6790,6 +6921,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.13.1",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -15,6 +15,7 @@ winres = "0.1"
|
||||
jni = "0.22.3"
|
||||
tauri = { version = "2.0.0", features = [] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
ndk-context = "0.1"
|
||||
log = "0.4"
|
||||
|
||||
@@ -22,6 +23,13 @@ tauri-plugin-vpn = { path = "./src/plugins/vpn-plugin" }
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
netrunner-logger = { git = "ssh://git@github.com/nineAp/netrunner-core.git", branch = "main", features = ["desktop"] }
|
||||
|
||||
# Автообновление есть только на десктопе — Android/iOS запрещают самообновление
|
||||
# в обход стора, поэтому эти крейты не тянутся и не регистрируются на мобильных
|
||||
# таргетах (см. src/lib.rs, #[cfg(desktop)]).
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2.0.0"
|
||||
tauri-plugin-process = "2.0.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "updater",
|
||||
"description": "Автообновление — только десктоп (Windows/macOS/Linux). Мобильные сборки обновляются через сторы.",
|
||||
"windows": ["main"],
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"permissions": [
|
||||
"updater:default",
|
||||
"process:allow-restart"
|
||||
]
|
||||
}
|
||||
+11
-2
@@ -1,8 +1,17 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_vpn::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_shell::init());
|
||||
|
||||
// Android/iOS не разрешают самообновление в обход стора — плагин
|
||||
// регистрируется только на десктопе (Windows/macOS/Linux).
|
||||
#[cfg(desktop)]
|
||||
let builder = builder
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"https://github.com/nineAp/netrunner-app/releases/latest/download/latest.json",
|
||||
"https://CHANGE_ME_GITEA_SERVER/nineAp/netrunner-app/releases/download/latest/latest.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDczRUU1OTM0MzM1N0M2MzkKUldRNXhsY3pORm51Yzl0YStpa1ZiWkgvWHByMTFBdlpCODJjWmt2UEEyMVI3WWg1MXZFZGt3ZmgK"
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcxQTFBM0JEMTk5OEEwQTAKUldTZ29KZ1p2YU9oY1JzTlZZeTlzTGFLS0lyVEdsZDRmODAySWZFajNISlpxdHhibTRUV01UZGkK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { NetrunnerMatrix } from "matrix-engine/NetrunnerMatrix";
|
||||
import { useSettingsStore } from "./store/useSettingsStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TelegramWeb } from "./pages/TelegramWeb";
|
||||
import { UpdateChecker } from "./features/UpdateChecker";
|
||||
import { ErrorBoundary } from "./features/ErrorBoundary";
|
||||
|
||||
function AppContent() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -102,6 +104,8 @@ function AppContent() {
|
||||
onClose={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
|
||||
<UpdateChecker />
|
||||
|
||||
<main className="flex-1 relative flex flex-col items-center justify-center overflow-hidden">
|
||||
<Routes>
|
||||
<Route path="/" element={isLoggedIn ? <Home /> : <Login />} />
|
||||
@@ -118,8 +122,10 @@ function AppContent() {
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ThemeProvider>
|
||||
<AppContent />
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface Props extends WithTranslation {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ловит непойманные ошибки рендера где угодно в дереве ниже — без неё любой
|
||||
* баг в компоненте (например, неожиданный ответ бэкенда, на который никто не
|
||||
* рассчитывал) валит всё приложение в белый экран без единого сообщения.
|
||||
* React error boundary обязан быть классовым компонентом — хуки этого не умеют.
|
||||
*/
|
||||
class ErrorBoundaryImpl extends Component<Props, State> {
|
||||
state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error("[ErrorBoundary] Непойманная ошибка рендера:", error, info.componentStack);
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
this.setState({ error: null });
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
if (!error) return this.props.children;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col items-center justify-center gap-4 bg-background p-6 text-center">
|
||||
<h1 className="text-lg font-semibold">{t("error_boundary_title")}</h1>
|
||||
<p className="max-w-sm text-sm text-muted-foreground">
|
||||
{t("error_boundary_desc")}
|
||||
</p>
|
||||
<Button onClick={this.handleReload}>{t("error_boundary_reload")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorBoundary = withTranslation()(ErrorBoundaryImpl);
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Update } from "@tauri-apps/plugin-updater";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Status = "idle" | "downloading" | "error";
|
||||
|
||||
/**
|
||||
* Проверка обновлений — только десктоп (Windows/macOS/Linux). На мобильных
|
||||
* сборках плагин не зарегистрирован (см. src-tauri/src/lib.rs, #[cfg(desktop)]),
|
||||
* поэтому check() там просто недоступен — ошибка тихо проглатывается ниже,
|
||||
* без падения остального приложения.
|
||||
*/
|
||||
export function UpdateChecker() {
|
||||
const { t } = useTranslation();
|
||||
const [update, setUpdate] = useState<Update | null>(null);
|
||||
const [status, setStatus] = useState<Status>("idle");
|
||||
const [progress, setProgress] = useState(0);
|
||||
const installing = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const { check } = await import("@tauri-apps/plugin-updater");
|
||||
const result = await check();
|
||||
if (!cancelled && result?.available) {
|
||||
setUpdate(result);
|
||||
}
|
||||
} catch {
|
||||
// Мобильная сборка или плагин недоступен в этом окружении — не критично.
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!update) return null;
|
||||
|
||||
const installUpdate = async () => {
|
||||
if (installing.current) return;
|
||||
installing.current = true;
|
||||
setStatus("downloading");
|
||||
try {
|
||||
let downloaded = 0;
|
||||
let total = 0;
|
||||
await update.downloadAndInstall((event) => {
|
||||
if (event.event === "Started") {
|
||||
total = event.data.contentLength ?? 0;
|
||||
} else if (event.event === "Progress") {
|
||||
downloaded += event.data.chunkLength;
|
||||
setProgress(total > 0 ? Math.round((downloaded / total) * 100) : 0);
|
||||
}
|
||||
});
|
||||
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||
await relaunch();
|
||||
} catch (e) {
|
||||
console.error("[UpdateChecker] Установка обновления не удалась:", e);
|
||||
setStatus("error");
|
||||
installing.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={() => {}}>
|
||||
<DialogContent showCloseButton={status !== "downloading"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("update_available_title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("update_available_desc", { version: update.version })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={installUpdate} disabled={status === "downloading"}>
|
||||
{status === "downloading"
|
||||
? t("update_downloading", { progress })
|
||||
: status === "error"
|
||||
? t("update_retry")
|
||||
: t("update_install_now")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
+52
-6
@@ -15,6 +15,52 @@ const API_V1 = `${API_BASE}/api/v1`;
|
||||
const SEED_KEY = "nrxp_seed";
|
||||
const TOKEN_KEY = "nrxp_token";
|
||||
|
||||
// --- Ретраи с backoff для сетевых сбоев ---
|
||||
// Раньше любой fetch() к бэкенду падал с первой же сетевой ошибки/5xx без
|
||||
// единой попытки повторить — временная просадка сети/перезапуск бэкенда сразу
|
||||
// приводили к нечитаемой ошибке в UI ("anon_failed" и т.п.). Ретраим только
|
||||
// сетевые сбои (fetch throws) и 5xx (backend сам сломан) — 4xx-ответы (401 на
|
||||
// просроченный код, 410 и т.д.) НЕ ретраим, это осмысленный ответ сервера,
|
||||
// а не транзиентный сбой, повторять его бессмысленно.
|
||||
const RETRY_ATTEMPTS = 2; // + первая попытка = до 3 всего
|
||||
const RETRY_BASE_DELAY_MS = 400;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export class NetworkUnavailableError extends Error {
|
||||
constructor(cause?: unknown) {
|
||||
super("NETWORK_UNAVAILABLE");
|
||||
this.name = "NetworkUnavailableError";
|
||||
if (cause) this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithRetry(
|
||||
input: string,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt <= RETRY_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
const res = await fetch(input, init);
|
||||
if (res.status >= 500 && attempt < RETRY_ATTEMPTS) {
|
||||
await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
|
||||
continue;
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
if (attempt < RETRY_ATTEMPTS) {
|
||||
await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new NetworkUnavailableError(lastError);
|
||||
}
|
||||
|
||||
// --- Доменная модель сервера (камелкейс-зеркало VpnNode из бэкенда) ---
|
||||
export interface ServerNode {
|
||||
id: string;
|
||||
@@ -89,7 +135,7 @@ function isTokenValid(token: string | null): token is string {
|
||||
}
|
||||
|
||||
async function generateSeed(): Promise<string> {
|
||||
const res = await fetch(`${API_V1}/auth/seed/generate`, {
|
||||
const res = await fetchWithRetry(`${API_V1}/auth/seed/generate`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -101,7 +147,7 @@ async function generateSeed(): Promise<string> {
|
||||
}
|
||||
|
||||
async function loginSeed(seed: string): Promise<string> {
|
||||
const res = await fetch(`${API_V1}/auth/seed/login`, {
|
||||
const res = await fetchWithRetry(`${API_V1}/auth/seed/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ seed }),
|
||||
@@ -133,7 +179,7 @@ async function ensureToken(): Promise<string> {
|
||||
|
||||
export async function fetchNodes(): Promise<ServerNode[]> {
|
||||
const token = await ensureToken();
|
||||
const res = await fetch(`${API_V1}/nodes`, {
|
||||
const res = await fetchWithRetry(`${API_V1}/nodes`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`GET /nodes failed: ${res.status}`);
|
||||
@@ -172,7 +218,7 @@ export async function loginWithSeed(seed: string): Promise<void> {
|
||||
* поэтому это одноразовый обмен, а не поллинг: `true` при первом же успешном вызове.
|
||||
*/
|
||||
export async function pollTelegramLogin(authCode: string): Promise<boolean> {
|
||||
const res = await fetch(`${API_V1}/auth/telegram/session/${authCode}`);
|
||||
const res = await fetchWithRetry(`${API_V1}/auth/telegram/session/${authCode}`);
|
||||
if (res.status === 401 || res.status === 410) {
|
||||
throw new Error("TELEGRAM_LOGIN_EXPIRED");
|
||||
}
|
||||
@@ -194,7 +240,7 @@ export interface UsageInfo {
|
||||
/** Текущий расход/лимит трафика юзера — для индикатора на `VpnStats`. */
|
||||
export async function fetchUsage(): Promise<UsageInfo> {
|
||||
const token = await ensureToken();
|
||||
const res = await fetch(`${API_V1}/users/me`, {
|
||||
const res = await fetchWithRetry(`${API_V1}/users/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`GET /users/me failed: ${res.status}`);
|
||||
@@ -212,7 +258,7 @@ let cachedBotUsername: string | null = null;
|
||||
export async function getBotUsername(): Promise<string | null> {
|
||||
if (cachedBotUsername) return cachedBotUsername;
|
||||
try {
|
||||
const res = await fetch(`${API_V1}/config`);
|
||||
const res = await fetchWithRetry(`${API_V1}/config`);
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { bot_username: string };
|
||||
cachedBotUsername = data.bot_username;
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "إعادة تعيين الاتصال",
|
||||
"splash_tagline_1": "النظام يراقب الجميع",
|
||||
"splash_tagline_2": "ما عداك",
|
||||
"dialog_close": "إغلاق"
|
||||
"dialog_close": "إغلاق",
|
||||
"update_available_title": "يتوفر تحديث",
|
||||
"update_available_desc": "الإصدار الجديد ({{version}}) جاهز للتثبيت.",
|
||||
"update_downloading": "جارٍ التنزيل... {{progress}}%",
|
||||
"update_install_now": "تثبيت وإعادة التشغيل",
|
||||
"update_retry": "إعادة المحاولة",
|
||||
"error_boundary_title": "حدث خطأ ما",
|
||||
"error_boundary_desc": "واجه التطبيق خطأ غير متوقع. عادةً ما تحل إعادة التحميل المشكلة.",
|
||||
"error_boundary_reload": "إعادة التحميل"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "СКІНУЦЬ ЗЛУЧЭННЕ",
|
||||
"splash_tagline_1": "Сістэма сочыць за ўсімі",
|
||||
"splash_tagline_2": "Акрамя цябе",
|
||||
"dialog_close": "Закрыць"
|
||||
"dialog_close": "Закрыць",
|
||||
"update_available_title": "Даступна абнаўленне",
|
||||
"update_available_desc": "Новая версія ({{version}}) гатовая да ўсталявання.",
|
||||
"update_downloading": "Загрузка... {{progress}}%",
|
||||
"update_install_now": "Усталяваць і перазапусціць",
|
||||
"update_retry": "Паўтарыць",
|
||||
"error_boundary_title": "Нешта пайшло не так",
|
||||
"error_boundary_desc": "Праграма сутыкнулася з нечаканай памылкай. Звычайна дапамагае перазагрузка.",
|
||||
"error_boundary_reload": "Перазагрузіць"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "RESET CONNECTION",
|
||||
"splash_tagline_1": "The System Watches Everyone",
|
||||
"splash_tagline_2": "Except You",
|
||||
"dialog_close": "Close"
|
||||
"dialog_close": "Close",
|
||||
"update_available_title": "Update Available",
|
||||
"update_available_desc": "A new version ({{version}}) is ready to install.",
|
||||
"update_downloading": "Downloading... {{progress}}%",
|
||||
"update_install_now": "Install & Restart",
|
||||
"update_retry": "Retry",
|
||||
"error_boundary_title": "Something went wrong",
|
||||
"error_boundary_desc": "The app hit an unexpected error. Reloading usually fixes it.",
|
||||
"error_boundary_reload": "Reload"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "بازنشانی اتصال",
|
||||
"splash_tagline_1": "سیستم همه را زیر نظر دارد",
|
||||
"splash_tagline_2": "بهجز تو",
|
||||
"dialog_close": "بستن"
|
||||
"dialog_close": "بستن",
|
||||
"update_available_title": "بهروزرسانی موجود است",
|
||||
"update_available_desc": "نسخه جدید ({{version}}) آماده نصب است.",
|
||||
"update_downloading": "در حال دانلود... {{progress}}%",
|
||||
"update_install_now": "نصب و راهاندازی مجدد",
|
||||
"update_retry": "تلاش مجدد",
|
||||
"error_boundary_title": "مشکلی پیش آمد",
|
||||
"error_boundary_desc": "برنامه با خطای غیرمنتظرهای مواجه شد. معمولاً بارگذاری مجدد آن را حل میکند.",
|
||||
"error_boundary_reload": "بارگذاری مجدد"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "СБРОСИТЬ СОЕДИНЕНИЕ",
|
||||
"splash_tagline_1": "Система следит за всеми",
|
||||
"splash_tagline_2": "Кроме тебя",
|
||||
"dialog_close": "Закрыть"
|
||||
"dialog_close": "Закрыть",
|
||||
"update_available_title": "Доступно обновление",
|
||||
"update_available_desc": "Готова к установке новая версия ({{version}}).",
|
||||
"update_downloading": "Загрузка... {{progress}}%",
|
||||
"update_install_now": "Установить и перезапустить",
|
||||
"update_retry": "Повторить",
|
||||
"error_boundary_title": "Что-то пошло не так",
|
||||
"error_boundary_desc": "Приложение столкнулось с неожиданной ошибкой. Обычно помогает перезагрузка.",
|
||||
"error_boundary_reload": "Перезагрузить"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "BAĞLANTIYI SIFIRLA",
|
||||
"splash_tagline_1": "Sistem Herkesi İzliyor",
|
||||
"splash_tagline_2": "Senin Dışında",
|
||||
"dialog_close": "Kapat"
|
||||
"dialog_close": "Kapat",
|
||||
"update_available_title": "Güncelleme mevcut",
|
||||
"update_available_desc": "Yeni sürüm ({{version}}) yüklenmeye hazır.",
|
||||
"update_downloading": "İndiriliyor... {{progress}}%",
|
||||
"update_install_now": "Yükle ve yeniden başlat",
|
||||
"update_retry": "Tekrar dene",
|
||||
"error_boundary_title": "Bir şeyler ters gitti",
|
||||
"error_boundary_desc": "Uygulama beklenmeyen bir hatayla karşılaştı. Genellikle yeniden yükleme sorunu çözer.",
|
||||
"error_boundary_reload": "Yeniden yükle"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "СКИНУТИ З'ЄДНАННЯ",
|
||||
"splash_tagline_1": "Система стежить за всіма",
|
||||
"splash_tagline_2": "Крім тебе",
|
||||
"dialog_close": "Закрити"
|
||||
"dialog_close": "Закрити",
|
||||
"update_available_title": "Доступне оновлення",
|
||||
"update_available_desc": "Нова версія ({{version}}) готова до встановлення.",
|
||||
"update_downloading": "Завантаження... {{progress}}%",
|
||||
"update_install_now": "Встановити й перезапустити",
|
||||
"update_retry": "Повторити",
|
||||
"error_boundary_title": "Щось пішло не так",
|
||||
"error_boundary_desc": "Застосунок зіткнувся з неочікуваною помилкою. Зазвичай допомагає перезавантаження.",
|
||||
"error_boundary_reload": "Перезавантажити"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "ULANISHNI QAYTA O'RNATISH",
|
||||
"splash_tagline_1": "Tizim Hammani Kuzatmoqda",
|
||||
"splash_tagline_2": "Sizdan Tashqari",
|
||||
"dialog_close": "Yopish"
|
||||
"dialog_close": "Yopish",
|
||||
"update_available_title": "Yangilanish mavjud",
|
||||
"update_available_desc": "Yangi versiya ({{version}}) o'rnatishga tayyor.",
|
||||
"update_downloading": "Yuklab olinmoqda... {{progress}}%",
|
||||
"update_install_now": "O'rnatish va qayta ishga tushirish",
|
||||
"update_retry": "Qayta urinish",
|
||||
"error_boundary_title": "Nimadir xato ketdi",
|
||||
"error_boundary_desc": "Ilovada kutilmagan xatolik yuz berdi. Odatda qayta yuklash yordam beradi.",
|
||||
"error_boundary_reload": "Qayta yuklash"
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "ĐẶT LẠI KẾT NỐI",
|
||||
"splash_tagline_1": "Hệ Thống Theo Dõi Mọi Người",
|
||||
"splash_tagline_2": "Trừ Bạn",
|
||||
"dialog_close": "Đóng"
|
||||
"dialog_close": "Đóng",
|
||||
"update_available_title": "Có bản cập nhật",
|
||||
"update_available_desc": "Phiên bản mới ({{version}}) đã sẵn sàng để cài đặt.",
|
||||
"update_downloading": "Đang tải xuống... {{progress}}%",
|
||||
"update_install_now": "Cài đặt và khởi động lại",
|
||||
"update_retry": "Thử lại",
|
||||
"error_boundary_title": "Đã xảy ra sự cố",
|
||||
"error_boundary_desc": "Ứng dụng gặp lỗi ngoài dự kiến. Tải lại thường sẽ khắc phục được.",
|
||||
"error_boundary_reload": "Tải lại"
|
||||
}
|
||||
|
||||
@@ -79,5 +79,13 @@
|
||||
"tg_reset_btn": "重置连接",
|
||||
"splash_tagline_1": "系统监视着所有人",
|
||||
"splash_tagline_2": "除了你",
|
||||
"dialog_close": "关闭"
|
||||
"dialog_close": "关闭",
|
||||
"update_available_title": "有可用更新",
|
||||
"update_available_desc": "新版本({{version}})已准备好安装。",
|
||||
"update_downloading": "正在下载... {{progress}}%",
|
||||
"update_install_now": "安装并重启",
|
||||
"update_retry": "重试",
|
||||
"error_boundary_title": "出了点问题",
|
||||
"error_boundary_desc": "应用遇到了意外错误。通常重新加载即可解决。",
|
||||
"error_boundary_reload": "重新加载"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user