mirror of
https://github.com/PRVTPRO/Amnezia-Web-Panel.git
synced 2026-09-18 01:44:10 +02:00
v1.6.0
Fixed merge requests Reset user traffic consumption Implemented automatic backups
This commit is contained in:
@@ -19,7 +19,7 @@ import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import signal
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import io
|
||||
from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse, StreamingResponse, FileResponse
|
||||
from starlette.background import BackgroundTask
|
||||
@@ -162,21 +162,39 @@ def load_data():
|
||||
data.setdefault('users', [])
|
||||
data.setdefault('user_connections', [])
|
||||
data.setdefault('api_tokens', [])
|
||||
data.setdefault('settings', {
|
||||
'appearance': {
|
||||
'title': 'Amnezia',
|
||||
'logo': '❤️',
|
||||
'subtitle': 'Web Panel'
|
||||
},
|
||||
'sync': {
|
||||
'remnawave_url': '',
|
||||
'remnawave_api_key': '',
|
||||
'remnawave_sync': False,
|
||||
'remnawave_sync_users': False,
|
||||
'remnawave_create_conns': False,
|
||||
'remnawave_server_id': 0,
|
||||
'remnawave_protocol': 'awg'
|
||||
}
|
||||
settings = data.setdefault('settings', {})
|
||||
settings.setdefault('appearance', {
|
||||
'title': 'Amnezia',
|
||||
'logo': '❤️',
|
||||
'subtitle': 'Web Panel'
|
||||
})
|
||||
settings.setdefault('sync', {
|
||||
'remnawave_url': '',
|
||||
'remnawave_api_key': '',
|
||||
'remnawave_sync': False,
|
||||
'remnawave_sync_users': False,
|
||||
'remnawave_create_conns': False,
|
||||
'remnawave_server_id': 0,
|
||||
'remnawave_protocol': 'awg'
|
||||
})
|
||||
settings.setdefault('captcha', {'enabled': False})
|
||||
settings.setdefault('telegram', {'token': '', 'enabled': False})
|
||||
settings.setdefault('ssl', {
|
||||
'enabled': False,
|
||||
'domain': '',
|
||||
'cert_path': '',
|
||||
'key_path': '',
|
||||
'cert_text': '',
|
||||
'key_text': '',
|
||||
'panel_port': 5000
|
||||
})
|
||||
settings.setdefault('auto_backup', {
|
||||
'enabled': False,
|
||||
'interval_hours': 24,
|
||||
'last_run_at': None,
|
||||
'last_status': None,
|
||||
'last_created_count': 0,
|
||||
'last_error': None
|
||||
})
|
||||
return data
|
||||
|
||||
@@ -1595,6 +1613,11 @@ class TelegramSettings(BaseModel):
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class AutoBackupSettings(BaseModel):
|
||||
enabled: bool = False
|
||||
interval_hours: int = 24
|
||||
|
||||
|
||||
|
||||
|
||||
class UpdateUserRequest(BaseModel):
|
||||
@@ -1614,6 +1637,7 @@ class SaveSettingsRequest(BaseModel):
|
||||
captcha: CaptchaSettings
|
||||
telegram: TelegramSettings
|
||||
ssl: SSLSettings
|
||||
auto_backup: AutoBackupSettings = AutoBackupSettings()
|
||||
|
||||
|
||||
class ToggleUserRequest(BaseModel):
|
||||
@@ -1705,20 +1729,26 @@ async def startup():
|
||||
changed = True
|
||||
logger.info("Initialised empty api_tokens collection")
|
||||
|
||||
# SSL settings migration
|
||||
if 'ssl' not in data.get('settings', {}):
|
||||
if 'settings' not in data: data['settings'] = {}
|
||||
data['settings']['ssl'] = {
|
||||
'enabled': False,
|
||||
'domain': '',
|
||||
'cert_path': '',
|
||||
'key_path': '',
|
||||
'cert_text': '',
|
||||
'key_text': '',
|
||||
'panel_port': 5000
|
||||
}
|
||||
# Auto backup settings migration
|
||||
auto_backup = data.setdefault('settings', {}).setdefault('auto_backup', {})
|
||||
if 'enabled' not in auto_backup:
|
||||
auto_backup['enabled'] = False
|
||||
changed = True
|
||||
if 'interval_hours' not in auto_backup:
|
||||
auto_backup['interval_hours'] = 24
|
||||
changed = True
|
||||
if 'last_run_at' not in auto_backup:
|
||||
auto_backup['last_run_at'] = None
|
||||
changed = True
|
||||
if 'last_status' not in auto_backup:
|
||||
auto_backup['last_status'] = None
|
||||
changed = True
|
||||
if 'last_created_count' not in auto_backup:
|
||||
auto_backup['last_created_count'] = 0
|
||||
changed = True
|
||||
if 'last_error' not in auto_backup:
|
||||
auto_backup['last_error'] = None
|
||||
changed = True
|
||||
logger.info("Migrated SSL settings")
|
||||
|
||||
if changed:
|
||||
save_data(data)
|
||||
@@ -1733,6 +1763,112 @@ async def startup():
|
||||
tg_bot.launch_bot(tg_cfg['token'], load_data, generate_vpn_link, save_data)
|
||||
|
||||
|
||||
def _auto_backup_due(auto_backup: dict, now: Optional[datetime] = None) -> bool:
|
||||
if not auto_backup.get('enabled'):
|
||||
return False
|
||||
try:
|
||||
interval_hours = max(1, min(24, int(auto_backup.get('interval_hours') or 24)))
|
||||
except (TypeError, ValueError):
|
||||
interval_hours = 24
|
||||
last_run_at = auto_backup.get('last_run_at')
|
||||
if not last_run_at:
|
||||
return True
|
||||
try:
|
||||
last_run = datetime.fromisoformat(str(last_run_at))
|
||||
except ValueError:
|
||||
return True
|
||||
now = now or datetime.now()
|
||||
return now - last_run >= timedelta(hours=interval_hours)
|
||||
|
||||
|
||||
def _create_auto_backups_once(data: dict) -> dict:
|
||||
started_at = datetime.now()
|
||||
created = []
|
||||
errors = []
|
||||
|
||||
for server_id, server in enumerate(data.get('servers', [])):
|
||||
protocols = server.get('protocols', {}) or {}
|
||||
installed_protocols = [
|
||||
proto for proto, info in protocols.items()
|
||||
if isinstance(info, dict) and info.get('installed') and is_valid_protocol(proto) and protocol_container_name(proto)
|
||||
]
|
||||
if not installed_protocols:
|
||||
continue
|
||||
|
||||
ssh = None
|
||||
try:
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
backup_manager = BackupManager(ssh)
|
||||
for proto in installed_protocols:
|
||||
try:
|
||||
result = backup_manager.create_backup(proto, protocol_container_name(proto))
|
||||
if result.get('status') == 'success':
|
||||
created.append({
|
||||
'server_id': server_id,
|
||||
'protocol': proto,
|
||||
'name': result.get('backup', {}).get('name')
|
||||
})
|
||||
else:
|
||||
errors.append({
|
||||
'server_id': server_id,
|
||||
'protocol': proto,
|
||||
'error': result.get('message', 'Failed to create backup')
|
||||
})
|
||||
except Exception as e:
|
||||
errors.append({'server_id': server_id, 'protocol': proto, 'error': str(e)})
|
||||
except Exception as e:
|
||||
for proto in installed_protocols:
|
||||
errors.append({'server_id': server_id, 'protocol': proto, 'error': str(e)})
|
||||
finally:
|
||||
if ssh:
|
||||
try:
|
||||
ssh.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
status = 'success' if not errors else ('partial' if created else 'error')
|
||||
return {
|
||||
'status': status,
|
||||
'started_at': started_at.isoformat(),
|
||||
'finished_at': datetime.now().isoformat(),
|
||||
'created_count': len(created),
|
||||
'created': created,
|
||||
'errors': errors,
|
||||
'error': '; '.join(f"server {e['server_id']} {e['protocol']}: {e['error']}" for e in errors[:5]) if errors else None
|
||||
}
|
||||
|
||||
|
||||
async def run_auto_backups_if_due():
|
||||
data = load_data()
|
||||
auto_backup = data.get('settings', {}).get('auto_backup', {})
|
||||
if not _auto_backup_due(auto_backup):
|
||||
return None
|
||||
|
||||
logger.info("Starting scheduled auto backups...")
|
||||
result = await asyncio.to_thread(_create_auto_backups_once, data)
|
||||
|
||||
async with DATA_LOCK:
|
||||
curr_data = load_data()
|
||||
curr_auto = curr_data.setdefault('settings', {}).setdefault('auto_backup', {})
|
||||
curr_auto['enabled'] = bool(curr_auto.get('enabled', auto_backup.get('enabled', False)))
|
||||
try:
|
||||
curr_auto['interval_hours'] = max(1, min(24, int(curr_auto.get('interval_hours') or auto_backup.get('interval_hours') or 24)))
|
||||
except (TypeError, ValueError):
|
||||
curr_auto['interval_hours'] = 24
|
||||
curr_auto['last_run_at'] = result['finished_at']
|
||||
curr_auto['last_status'] = result['status']
|
||||
curr_auto['last_created_count'] = result['created_count']
|
||||
curr_auto['last_error'] = result.get('error')
|
||||
save_data(curr_data)
|
||||
|
||||
logger.info(
|
||||
"Auto backups finished: status=%s created=%s errors=%s",
|
||||
result['status'], result['created_count'], len(result.get('errors', []))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _scrape_server_traffic(server, sid, my_conns):
|
||||
server_updates = []
|
||||
try:
|
||||
@@ -1851,7 +1987,10 @@ async def periodic_background_tasks():
|
||||
logger.info(f"Traffic limit reached, disabling users: {to_disable_uids}")
|
||||
await perform_mass_operations(toggle_uids=[(uid, False) for uid in to_disable_uids])
|
||||
|
||||
# --- 2. REMNAWAVE SYNC ---
|
||||
# --- 2. AUTO BACKUP ---
|
||||
await run_auto_backups_if_due()
|
||||
|
||||
# --- 3. REMNAWAVE SYNC ---
|
||||
logger.info("Starting background Remnawave sync...")
|
||||
data = load_data()
|
||||
if data.get('settings', {}).get('sync', {}).get('remnawave_sync_users'):
|
||||
@@ -3436,6 +3575,29 @@ async def api_toggle_user(request: Request, user_id: str, req: ToggleUserRequest
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.post('/api/users/{user_id}/traffic/reset', tags=["Users"])
|
||||
async def api_reset_user_traffic(request: Request, user_id: str):
|
||||
if not _check_admin(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
try:
|
||||
data = load_data()
|
||||
user = next((u for u in data.get('users', []) if u.get('id') == user_id), None)
|
||||
if not user:
|
||||
return JSONResponse({'error': 'User not found'}, status_code=404)
|
||||
|
||||
user['traffic_used'] = 0
|
||||
user['last_reset_at'] = datetime.now().isoformat()
|
||||
save_data(data)
|
||||
return {
|
||||
'status': 'success',
|
||||
'traffic_used': user['traffic_used'],
|
||||
'last_reset_at': user['last_reset_at']
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception("Error resetting user traffic")
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.post('/api/users/{user_id}/connections/add', tags=["Users"])
|
||||
async def api_add_user_connection(request: Request, user_id: str, req: AddUserConnectionRequest):
|
||||
if not _check_admin(request):
|
||||
@@ -3806,13 +3968,25 @@ async def save_settings(request: Request, payload: SaveSettingsRequest):
|
||||
if not _check_admin(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
data = load_data()
|
||||
data['settings']['appearance'] = payload.appearance.dict()
|
||||
data['settings']['sync'] = payload.sync.dict()
|
||||
data['settings']['captcha'] = payload.captcha.dict()
|
||||
data['settings']['telegram'] = payload.telegram.dict()
|
||||
data['settings']['ssl'] = payload.ssl.dict()
|
||||
settings = data.setdefault('settings', {})
|
||||
settings['appearance'] = payload.appearance.dict()
|
||||
settings['sync'] = payload.sync.dict()
|
||||
settings['captcha'] = payload.captcha.dict()
|
||||
settings['telegram'] = payload.telegram.dict()
|
||||
settings['ssl'] = payload.ssl.dict()
|
||||
|
||||
old_auto_backup = settings.get('auto_backup', {}) or {}
|
||||
interval_hours = max(1, min(24, int(payload.auto_backup.interval_hours or 24)))
|
||||
settings['auto_backup'] = {
|
||||
'enabled': bool(payload.auto_backup.enabled),
|
||||
'interval_hours': interval_hours,
|
||||
'last_run_at': old_auto_backup.get('last_run_at'),
|
||||
'last_status': old_auto_backup.get('last_status'),
|
||||
'last_created_count': old_auto_backup.get('last_created_count', 0),
|
||||
'last_error': old_auto_backup.get('last_error')
|
||||
}
|
||||
save_data(data)
|
||||
logger.info("Settings saved (including captcha and telegram)")
|
||||
logger.info("Settings saved (including captcha, telegram and auto backup)")
|
||||
|
||||
# Handle bot start/stop based on new telegram settings
|
||||
tg_cfg = payload.telegram
|
||||
|
||||
@@ -1278,10 +1278,10 @@ AllowedIPs = {allowed_ips}
|
||||
|
||||
# Standard fields (dual-stack when the client has an IPv6 address)
|
||||
address_line = f"{client_ip}/32" + (f", {client_ipv6}/128" if client_ipv6 else "")
|
||||
dns_line = f"{dns1}, {dns2}" + (", 2606:4700:4700::1111" if client_ipv6 else "")
|
||||
dns_line = dns + (", 2606:4700:4700::1111" if client_ipv6 else "")
|
||||
config_lines = [
|
||||
f"Address = {client_ip}/32",
|
||||
f"DNS = {dns}",
|
||||
f"Address = {address_line}",
|
||||
f"DNS = {dns_line}",
|
||||
f"PrivateKey = {client_priv_key}",
|
||||
f"MTU = {mtu}"
|
||||
]
|
||||
@@ -1370,10 +1370,10 @@ PersistentKeepalive = 25
|
||||
|
||||
# Standard fields (dual-stack when the client has an IPv6 address)
|
||||
address_line = f"{client_ip}/32" + (f", {client_ipv6}/128" if client_ipv6 else "")
|
||||
dns_line = f"{dns1}, {dns2}" + (", 2606:4700:4700::1111" if client_ipv6 else "")
|
||||
dns_line = dns + (", 2606:4700:4700::1111" if client_ipv6 else "")
|
||||
config_lines = [
|
||||
f"Address = {client_ip}/32",
|
||||
f"DNS = {dns}",
|
||||
f"Address = {address_line}",
|
||||
f"DNS = {dns_line}",
|
||||
f"PrivateKey = {client_priv_key}",
|
||||
f"MTU = {mtu}"
|
||||
]
|
||||
|
||||
@@ -48,7 +48,7 @@ class BackupManager:
|
||||
def inst_path(path, suffix_fmt='-{idx}'):
|
||||
return path if idx <= 1 else f"{path}{suffix_fmt.format(idx=idx)}"
|
||||
|
||||
if base in ('awg', 'awg2', 'awg_legacy'):
|
||||
if base in ('awg', 'awg2', 'awg3', 'awg_legacy'):
|
||||
paths['host'] = ['/opt/amnezia/awg', f'/opt/amnezia/{container_name}']
|
||||
paths['container'] = ['/opt/amnezia/awg', '/opt/amnezia/start.sh']
|
||||
elif base == 'wireguard':
|
||||
|
||||
+41
-1
@@ -360,6 +360,40 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px solid var(--border-color); padding-top: var(--space-md);">
|
||||
<h4 style="margin: 0 0 var(--space-sm) 0; font-size: 0.95rem;">{{ _('auto_backup_title') }}</h4>
|
||||
<form id="autoBackupForm">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: var(--space-sm); cursor: pointer;">
|
||||
<input type="checkbox" name="enabled" id="autoBackupEnabled" {% if settings.auto_backup.enabled %}checked{% endif %}>
|
||||
{{ _('enable_auto_backup') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('auto_backup_interval_label') }}</label>
|
||||
<select class="form-select" name="interval_hours" id="autoBackupInterval">
|
||||
{% set auto_backup_interval = settings.get('auto_backup', {}).get('interval_hours', 24) | int %}
|
||||
{% for hour in range(1, 25) %}
|
||||
<option value="{{ hour }}" {% if auto_backup_interval == hour %}selected{% endif %}>{{ hour }} {{ _('hours_suffix') }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<p class="form-hint">{{ _('auto_backup_hint') }}</p>
|
||||
{% if settings.auto_backup.last_run_at %}
|
||||
<div class="form-hint" style="margin-top: var(--space-xs);">
|
||||
{{ _('auto_backup_last_run') }}: {{ settings.auto_backup.last_run_at }} ·
|
||||
{{ _('auto_backup_last_status') }}: {{ settings.auto_backup.last_status or '-' }} ·
|
||||
{{ _('auto_backup_created_count') }}: {{ settings.auto_backup.last_created_count or 0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.auto_backup.last_error %}
|
||||
<div class="form-hint" style="margin-top: var(--space-xs); color: var(--danger);">
|
||||
{{ settings.auto_backup.last_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-hint" style="margin-top: var(--space-sm);">
|
||||
{{ _('restore_confirm') }}
|
||||
@@ -805,11 +839,17 @@
|
||||
panel_port: parseInt(document.getElementById('panel_port').value || '5000')
|
||||
};
|
||||
|
||||
const autoBackupForm = document.getElementById('autoBackupForm');
|
||||
const auto_backup = {
|
||||
enabled: autoBackupForm.enabled.checked,
|
||||
interval_hours: parseInt(autoBackupForm.interval_hours.value || '24')
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settings/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ appearance, sync, captcha, telegram, ssl })
|
||||
body: JSON.stringify({ appearance, sync, captcha, telegram, ssl, auto_backup })
|
||||
});
|
||||
if (!res.ok) throw new Error(_('error'));
|
||||
showToast(_('settings_saved'), 'success');
|
||||
|
||||
+36
-6
@@ -213,12 +213,18 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('traffic_reset_strategy_label') }}</label>
|
||||
<select class="form-select" id="editUserTrafficResetStrategy">
|
||||
<option value="never">{{ _('traffic_reset_never') }}</option>
|
||||
<option value="daily">{{ _('traffic_reset_daily') }}</option>
|
||||
<option value="weekly">{{ _('traffic_reset_weekly') }}</option>
|
||||
<option value="monthly">{{ _('traffic_reset_monthly') }}</option>
|
||||
</select>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; gap: var(--space-sm); align-items: center;">
|
||||
<select class="form-select" id="editUserTrafficResetStrategy">
|
||||
<option value="never">{{ _('traffic_reset_never') }}</option>
|
||||
<option value="daily">{{ _('traffic_reset_daily') }}</option>
|
||||
<option value="weekly">{{ _('traffic_reset_weekly') }}</option>
|
||||
<option value="monthly">{{ _('traffic_reset_monthly') }}</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="resetTrafficBtn" onclick="resetUserTraffic()">
|
||||
<span id="resetTrafficBtnText">{{ _('reset_traffic') }}</span>
|
||||
<div class="spinner hidden" id="resetTrafficSpinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -985,6 +991,30 @@
|
||||
openModal('editUserModal');
|
||||
}
|
||||
|
||||
async function resetUserTraffic() {
|
||||
const uid = document.getElementById('editUserId').value;
|
||||
if (!uid || !confirm(_('reset_traffic_confirm'))) return;
|
||||
|
||||
const btn = document.getElementById('resetTrafficBtn');
|
||||
const text = document.getElementById('resetTrafficBtnText');
|
||||
const spinner = document.getElementById('resetTrafficSpinner');
|
||||
btn.disabled = true;
|
||||
text.textContent = _('resetting');
|
||||
spinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
await apiCall(`/api/users/${uid}/traffic/reset`, 'POST');
|
||||
showToast(_('traffic_reset_success'), 'success');
|
||||
refreshUsersList();
|
||||
} catch (err) {
|
||||
showToast(`${_('error')}: ` + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
text.textContent = _('reset_traffic');
|
||||
spinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEditUser() {
|
||||
const uid = document.getElementById('editUserId').value;
|
||||
const btn = document.getElementById('saveEditBtn');
|
||||
|
||||
+13
-1
@@ -262,6 +262,14 @@
|
||||
"restore_backup": "Restore from file",
|
||||
"restore_confirm": "Are you sure? Current data will be overwritten and the application will restart.",
|
||||
"restore_success": "Restore successful! Restarting...",
|
||||
"auto_backup_title": "Auto backup",
|
||||
"enable_auto_backup": "Enable server auto backups",
|
||||
"auto_backup_interval_label": "Creation interval",
|
||||
"auto_backup_hint": "The panel will create backups of installed protocols on every server at the selected interval.",
|
||||
"auto_backup_last_run": "Last run",
|
||||
"auto_backup_last_status": "Status",
|
||||
"auto_backup_created_count": "Backups created",
|
||||
"hours_suffix": "h",
|
||||
"invalid_backup_file": "Invalid backup file or structure",
|
||||
"config_unavailable": "Configuration unavailable",
|
||||
"config_unavailable_desc": "This client was created via the native Amnezia app.\\nThe private key is stored only on the user's device and cannot be recovered by the server.",
|
||||
@@ -424,5 +432,9 @@
|
||||
"rename": "Rename",
|
||||
"connection_renamed": "Connection renamed",
|
||||
"enter_connection_name": "Enter a new connection name",
|
||||
"config_saved": "Config saved"
|
||||
"config_saved": "Config saved",
|
||||
"reset_traffic": "Reset traffic",
|
||||
"resetting": "Resetting...",
|
||||
"reset_traffic_confirm": "Reset this user's current traffic? Total statistics will be kept.",
|
||||
"traffic_reset_success": "Traffic reset"
|
||||
}
|
||||
+14
-2
@@ -259,7 +259,15 @@
|
||||
"download_backup": "دانلود data.json",
|
||||
"restore_backup": "بازیابی از فایل",
|
||||
"restore_confirm": "آیا مطمئن هستید؟ دادههای فعلی بازنویسی میشوند و برنامه دوباره راهاندازی خواهد شد.",
|
||||
"restore_success": "بازیابی موفقیتآمیز بود! در حال راهاندازی مجدد...",
|
||||
"restore_success": "بازیابی موفق بود! در حال راهاندازی مجدد...",
|
||||
"auto_backup_title": "Auto backup",
|
||||
"enable_auto_backup": "فعالسازی پشتیبانگیری خودکار سرورها",
|
||||
"auto_backup_interval_label": "فاصله زمانی ایجاد",
|
||||
"auto_backup_hint": "پنل از پروتکلهای نصبشده روی هر سرور در بازه انتخابشده پشتیبان میگیرد.",
|
||||
"auto_backup_last_run": "آخرین اجرا",
|
||||
"auto_backup_last_status": "وضعیت",
|
||||
"auto_backup_created_count": "پشتیبانهای ایجادشده",
|
||||
"hours_suffix": "ساعت",
|
||||
"invalid_backup_file": "فایل پشتیبان یا ساختار نامعتبر است",
|
||||
"config_unavailable": "پیکربندی در دسترس نیست",
|
||||
"config_unavailable_desc": "این کلاینت از طریق اپلیکیشن اصلی Amnezia ایجاد شده است.\\nکلید خصوصی فقط در دستگاه کاربر ذخیره میشود و توسط سرور قابل بازیابی نیست.",
|
||||
@@ -391,5 +399,9 @@
|
||||
"rename": "تغییر نام",
|
||||
"connection_renamed": "اتصال تغییر نام یافت",
|
||||
"enter_connection_name": "نام جدید اتصال را وارد کنید",
|
||||
"config_saved": "پیکربندی ذخیره شد"
|
||||
"config_saved": "پیکربندی ذخیره شد",
|
||||
"reset_traffic": "بازنشانی ترافیک",
|
||||
"resetting": "در حال بازنشانی...",
|
||||
"reset_traffic_confirm": "ترافیک فعلی این کاربر بازنشانی شود؟ آمار کل حفظ میشود.",
|
||||
"traffic_reset_success": "ترافیک بازنشانی شد"
|
||||
}
|
||||
+14
-2
@@ -260,7 +260,15 @@
|
||||
"restore_backup": "Restaurer depuis un fichier",
|
||||
"restore_confirm": "Êtes-vous sûr ? Les données actuelles seront écrasées et l'application redémarrera.",
|
||||
"restore_success": "Restauration réussie ! Redémarrage...",
|
||||
"invalid_backup_file": "Fichier de sauvegarde ou structure invalide",
|
||||
"auto_backup_title": "Auto backup",
|
||||
"enable_auto_backup": "Activer les sauvegardes automatiques des serveurs",
|
||||
"auto_backup_interval_label": "Intervalle de création",
|
||||
"auto_backup_hint": "Le panneau créera des sauvegardes des protocoles installés sur chaque serveur selon l'intervalle choisi.",
|
||||
"auto_backup_last_run": "Dernier lancement",
|
||||
"auto_backup_last_status": "Statut",
|
||||
"auto_backup_created_count": "Sauvegardes créées",
|
||||
"hours_suffix": "h",
|
||||
"invalid_backup_file": "Fichier ou structure de sauvegarde invalide",
|
||||
"config_unavailable": "Configuration indisponible",
|
||||
"config_unavailable_desc": "Ce client a été créé via l'application native Amnezia.\\nLa clé privée est stockée uniquement sur l'appareil de l'utilisateur et ne peut pas être récupérée par le serveur.",
|
||||
"client_public_key": "Clé publique du client :",
|
||||
@@ -391,5 +399,9 @@
|
||||
"rename": "Renommer",
|
||||
"connection_renamed": "Connexion renommée",
|
||||
"enter_connection_name": "Saisir un nouveau nom de connexion",
|
||||
"config_saved": "Configuration enregistrée"
|
||||
"config_saved": "Configuration enregistrée",
|
||||
"reset_traffic": "Réinitialiser le trafic",
|
||||
"resetting": "Réinitialisation...",
|
||||
"reset_traffic_confirm": "Réinitialiser le trafic actuel de cet utilisateur ? Les statistiques totales seront conservées.",
|
||||
"traffic_reset_success": "Trafic réinitialisé"
|
||||
}
|
||||
+13
-1
@@ -262,6 +262,14 @@
|
||||
"restore_backup": "Восстановить из файла",
|
||||
"restore_confirm": "Вы уверены? Текущие данные будут перезаписаны, и приложение перезагрузится.",
|
||||
"restore_success": "Восстановление успешно! Перезагрузка...",
|
||||
"auto_backup_title": "Auto backup",
|
||||
"enable_auto_backup": "Включить автобекапы серверов",
|
||||
"auto_backup_interval_label": "Интервал создания",
|
||||
"auto_backup_hint": "Панель будет создавать бекапы установленных протоколов на каждом сервере с выбранным интервалом.",
|
||||
"auto_backup_last_run": "Последний запуск",
|
||||
"auto_backup_last_status": "Статус",
|
||||
"auto_backup_created_count": "Создано бекапов",
|
||||
"hours_suffix": "ч",
|
||||
"invalid_backup_file": "Неверный файл резервной копии или структура",
|
||||
"config_unavailable": "Конфигурация недоступна",
|
||||
"config_unavailable_desc": "Этот клиент был создан через нативное приложение Amnezia.\\nПриватный ключ хранится только на устройстве пользователя и не может быть восстановлен сервером.",
|
||||
@@ -424,5 +432,9 @@
|
||||
"rename": "Переименовать",
|
||||
"connection_renamed": "Подключение переименовано",
|
||||
"enter_connection_name": "Введите новое имя подключения",
|
||||
"config_saved": "Конфиг сохранён"
|
||||
"config_saved": "Конфиг сохранён",
|
||||
"reset_traffic": "Сбросить трафик",
|
||||
"resetting": "Сброс...",
|
||||
"reset_traffic_confirm": "Сбросить текущий трафик пользователя? Общая статистика сохранится.",
|
||||
"traffic_reset_success": "Трафик сброшен"
|
||||
}
|
||||
+14
-2
@@ -260,7 +260,15 @@
|
||||
"restore_backup": "从文件恢复",
|
||||
"restore_confirm": "您确定吗?当前数据将被覆盖,应用程序将重新启动。",
|
||||
"restore_success": "恢复成功!正在重启...",
|
||||
"invalid_backup_file": "备份文件无效或结构错误",
|
||||
"auto_backup_title": "Auto backup",
|
||||
"enable_auto_backup": "启用服务器自动备份",
|
||||
"auto_backup_interval_label": "创建间隔",
|
||||
"auto_backup_hint": "面板会按所选间隔为每台服务器上已安装的协议创建备份。",
|
||||
"auto_backup_last_run": "上次运行",
|
||||
"auto_backup_last_status": "状态",
|
||||
"auto_backup_created_count": "已创建备份",
|
||||
"hours_suffix": "小时",
|
||||
"invalid_backup_file": "备份文件或结构无效",
|
||||
"config_unavailable": "配置文件不可用",
|
||||
"config_unavailable_desc": "此客户端是通过 Amnezia 原生应用创建的。\\n私钥仅存储在用户设备上,服务器无法恢复。",
|
||||
"client_public_key": "客户端公钥:",
|
||||
@@ -391,5 +399,9 @@
|
||||
"rename": "重命名",
|
||||
"connection_renamed": "连接已重命名",
|
||||
"enter_connection_name": "请输入新的连接名称",
|
||||
"config_saved": "配置已保存"
|
||||
"config_saved": "配置已保存",
|
||||
"reset_traffic": "重置流量",
|
||||
"resetting": "正在重置...",
|
||||
"reset_traffic_confirm": "重置此用户的当前流量?总统计将保留。",
|
||||
"traffic_reset_success": "流量已重置"
|
||||
}
|
||||
Reference in New Issue
Block a user