Merge branch 'main' into feat/daily-traffic-speed

This commit is contained in:
stivfilippov 2026-05-08 14:02:36 +03:00 committed by GitHub
commit e3662f482f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 191 additions and 26 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,8 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/web/entity"
"github.com/mhsanaei/3x-ui/v2/web/global" "github.com/mhsanaei/3x-ui/v2/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/websocket" "github.com/mhsanaei/3x-ui/v2/web/websocket"
@ -135,10 +137,17 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
} }
// getPanelUpdateInfo retrieves the current and latest panel version. // getPanelUpdateInfo retrieves the current and latest panel version.
// Network failures (e.g. no internet, GitHub blocked) are logged at debug
// level only — the panel keeps working offline and we don't want to spam
// WARN every time a user opens the page.
func (a *ServerController) getPanelUpdateInfo(c *gin.Context) { func (a *ServerController) getPanelUpdateInfo(c *gin.Context) {
info, err := a.panelService.GetUpdateInfo() info, err := a.panelService.GetUpdateInfo()
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.index.panelUpdateCheckPopover"), err) logger.Debug("panel update check failed:", err)
c.JSON(http.StatusOK, entity.Msg{
Success: false,
Msg: I18nWeb(c, "pages.index.panelUpdateCheckPopover"),
})
return return
} }
jsonObj(c, info, nil) jsonObj(c, info, nil)

View file

@ -3,12 +3,98 @@
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<style> <style>
.custom-geo-section code.custom-geo-ext-code {
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
background: rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.08);
}
.custom-geo-copyable {
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.custom-geo-copyable:hover {
background: rgba(24, 144, 255, 0.12);
border-color: rgba(24, 144, 255, 0.45);
}
.custom-geo-alias-cell {
display: flex;
align-items: center;
gap: 6px;
}
.custom-geo-alias {
font-weight: 500;
}
.custom-geo-type-tag {
margin-right: 0;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.4px;
}
.custom-geo-url {
display: inline-block;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.custom-geo-muted {
color: rgba(0, 0, 0, 0.35);
}
.custom-geo-count {
background: rgba(0, 0, 0, 0.06);
color: rgba(0, 0, 0, 0.55);
border-radius: 10px;
padding: 1px 8px;
font-size: 12px;
}
.custom-geo-empty {
padding: 24px 0;
color: rgba(0, 0, 0, 0.45);
text-align: center;
}
.custom-geo-empty-icon {
font-size: 32px;
color: rgba(0, 0, 0, 0.25);
display: block;
margin: 0 auto 8px;
}
body.dark .custom-geo-section code.custom-geo-ext-code { body.dark .custom-geo-section code.custom-geo-ext-code {
color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.85)); color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.85));
background: var(--dark-color-surface-200, #222d42); background: var(--dark-color-surface-200, #222d42);
border: 1px solid var(--dark-color-stroke, #2c3950); border: 1px solid var(--dark-color-stroke, #2c3950);
padding: 2px 6px; }
border-radius: 3px;
body.dark .custom-geo-copyable:hover {
background: rgba(24, 144, 255, 0.18);
border-color: rgba(64, 169, 255, 0.55);
}
body.dark .custom-geo-muted,
body.dark .custom-geo-empty {
color: rgba(255, 255, 255, 0.45);
}
body.dark .custom-geo-empty-icon {
color: rgba(255, 255, 255, 0.25);
}
body.dark .custom-geo-count {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.7);
} }
html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code { html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code {
@ -383,21 +469,43 @@
<div class="custom-geo-section"> <div class="custom-geo-section">
<a-alert type="info" show-icon class="mb-10" <a-alert type="info" show-icon class="mb-10"
message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert> message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
<div class="mb-10"> <div class="mb-10 d-flex align-center" style="flex-wrap: wrap; gap: 8px;">
<a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading"> <a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
{{ i18n "pages.index.customGeoAdd" }} {{ i18n "pages.index.customGeoAdd" }}
</a-button> </a-button>
<a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n <a-button icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll"
"pages.index.geofilesUpdateAll" }}</a-button> :disabled="!customGeoList.length">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button>
<span v-if="customGeoList.length" class="custom-geo-count">[[ customGeoList.length ]]</span>
</div> </div>
<a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id" <a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
:loading="customGeoLoading" size="small" :scroll="{ x: 520 }"> :loading="customGeoLoading" size="small" :scroll="{ x: 760 }">
<template slot="alias" slot-scope="text, record">
<div class="custom-geo-alias-cell">
<a-tag :color="record.type === 'geoip' ? 'cyan' : 'purple'"
class="custom-geo-type-tag">[[ record.type ]]</a-tag>
<span class="custom-geo-alias">[[ record.alias ]]</span>
</div>
</template>
<template slot="url" slot-scope="text, record">
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme" placement="topLeft">
<template slot="title">[[ record.url ]]</template>
<a :href="record.url" target="_blank" rel="noopener noreferrer"
class="custom-geo-url">[[ record.url ]]</a>
</a-tooltip>
</template>
<template slot="extDat" slot-scope="text, record"> <template slot="extDat" slot-scope="text, record">
<code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "copy" }}</template>
<code class="custom-geo-ext-code custom-geo-copyable"
@click="copyCustomGeoExt(record)">[[ customGeoExtDisplay(record) ]]</code>
</a-tooltip>
</template> </template>
<template slot="lastUpdatedAt" slot-scope="text, record"> <template slot="lastUpdatedAt" slot-scope="text, record">
<span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span> <a-tooltip v-if="record.lastUpdatedAt" :overlay-class-name="themeSwitcher.currentTheme">
<span v-else></span> <template slot="title">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</template>
<span>[[ customGeoRelativeTime(record.lastUpdatedAt) ]]</span>
</a-tooltip>
<span v-else class="custom-geo-muted"></span>
</template> </template>
<template slot="action" slot-scope="text, record"> <template slot="action" slot-scope="text, record">
<a-space size="small"> <a-space size="small">
@ -416,6 +524,12 @@
</a-tooltip> </a-tooltip>
</a-space> </a-space>
</template> </template>
<template slot="emptyText">
<div class="custom-geo-empty">
<a-icon type="inbox" class="custom-geo-empty-icon"></a-icon>
<div>{{ i18n "pages.index.customGeoEmpty" }}</div>
</div>
</template>
</a-table> </a-table>
</div> </div>
</a-collapse-panel> </a-collapse-panel>
@ -1111,29 +1225,34 @@
}; };
const customGeoColumns = [{ const customGeoColumns = [{
title: '{{ i18n "pages.index.customGeoAlias" }}',
key: 'alias',
scopedSlots: { customRender: 'alias' },
width: 200
},
{
title: '{{ i18n "pages.index.customGeoUrl" }}',
key: 'url',
scopedSlots: { customRender: 'url' },
ellipsis: true
},
{
title: '{{ i18n "pages.index.customGeoExtColumn" }}', title: '{{ i18n "pages.index.customGeoExtColumn" }}',
key: 'extDat', key: 'extDat',
scopedSlots: { scopedSlots: { customRender: 'extDat' },
customRender: 'extDat' width: 220
},
ellipsis: true
}, },
{ {
title: '{{ i18n "pages.index.customGeoLastUpdated" }}', title: '{{ i18n "pages.index.customGeoLastUpdated" }}',
key: 'lastUpdatedAt', key: 'lastUpdatedAt',
scopedSlots: { scopedSlots: { customRender: 'lastUpdatedAt' },
customRender: 'lastUpdatedAt' width: 140
},
width: 160
}, },
{ {
title: '{{ i18n "pages.index.customGeoActions" }}', title: '{{ i18n "pages.index.customGeoActions" }}',
key: 'action', key: 'action',
scopedSlots: { scopedSlots: { customRender: 'action' },
customRender: 'action'
},
width: 120, width: 120,
fixed: 'right'
}, },
]; ];
@ -1266,12 +1385,29 @@
if (!ts) return ''; if (!ts) return '';
return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts); return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts);
}, },
customGeoRelativeTime(ts) {
if (!ts) return '';
if (typeof moment === 'undefined') return String(ts);
return moment(ts * 1000).fromNow();
},
customGeoExtDisplay(record) { customGeoExtDisplay(record) {
const fn = record.type === 'geoip' ? const fn = record.type === 'geoip' ?
`geoip_${record.alias}.dat` : `geoip_${record.alias}.dat` :
`geosite_${record.alias}.dat`; `geosite_${record.alias}.dat`;
return `ext:${fn}:tag`; return `ext:${fn}:tag`;
}, },
copyCustomGeoExt(record) {
const text = this.customGeoExtDisplay(record);
if (typeof ClipboardManager !== 'undefined' && ClipboardManager.copyText) {
ClipboardManager.copyText(text).then(ok => {
if (ok) this.$message.success(`{{ i18n "copy" }}: ${text}`);
});
} else if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success(`{{ i18n "copy" }}: ${text}`);
});
}
},
async loadCustomGeo() { async loadCustomGeo() {
this.customGeoLoading = true; this.customGeoLoading = true;
try { try {
@ -1376,8 +1512,13 @@
this.customGeoUpdatingAll = true; this.customGeoUpdatingAll = true;
try { try {
const msg = await HttpUtil.post('/panel/api/custom-geo/update-all'); const msg = await HttpUtil.post('/panel/api/custom-geo/update-all');
if (msg.success || (msg.obj && Array.isArray(msg.obj.succeeded) && msg.obj.succeeded.length > 0)) { const ok = (msg && msg.obj && Array.isArray(msg.obj.succeeded)) ? msg.obj.succeeded.length : 0;
const failed = (msg && msg.obj && Array.isArray(msg.obj.failed)) ? msg.obj.failed.length : 0;
if (msg.success || ok > 0) {
await this.loadCustomGeo(); await this.loadCustomGeo();
if (failed > 0) {
this.$message.warning(`Updated ${ok}, failed ${failed}`);
}
} }
} finally { } finally {
this.customGeoUpdatingAll = false; this.customGeoUpdatingAll = false;

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "مصدر geo المخصص غير موجود" "customGeoErrNotFound" = "مصدر geo المخصص غير موجود"
"customGeoErrDownload" = "فشل التنزيل" "customGeoErrDownload" = "فشل التنزيل"
"customGeoErrUpdateAllIncomplete" = "تعذر تحديث مصدر واحد أو أكثر من مصادر geo المخصصة" "customGeoErrUpdateAllIncomplete" = "تعذر تحديث مصدر واحد أو أكثر من مصادر geo المخصصة"
"customGeoEmpty" = "لا توجد مصادر geo مخصصة بعد — انقر على «إضافة» لإنشاء واحد"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "إجمالي حركة المرور" "allTimeTraffic" = "إجمالي حركة المرور"

View file

@ -204,6 +204,7 @@
"customGeoErrNotFound" = "Custom geo source not found" "customGeoErrNotFound" = "Custom geo source not found"
"customGeoErrDownload" = "Download failed" "customGeoErrDownload" = "Download failed"
"customGeoErrUpdateAllIncomplete" = "One or more custom geo sources failed to update" "customGeoErrUpdateAllIncomplete" = "One or more custom geo sources failed to update"
"customGeoEmpty" = "No custom geo sources yet — click Add to create one"
"dontRefresh" = "Installation is in progress, please do not refresh this page" "dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs" "logs" = "Logs"
"config" = "Config" "config" = "Config"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Fuente geo personalizada no encontrada" "customGeoErrNotFound" = "Fuente geo personalizada no encontrada"
"customGeoErrDownload" = "Error de descarga" "customGeoErrDownload" = "Error de descarga"
"customGeoErrUpdateAllIncomplete" = "No se pudieron actualizar una o más fuentes geo personalizadas" "customGeoErrUpdateAllIncomplete" = "No se pudieron actualizar una o más fuentes geo personalizadas"
"customGeoEmpty" = "Aún no hay fuentes geo personalizadas — haz clic en Añadir para crear una"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tráfico Total" "allTimeTraffic" = "Tráfico Total"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "منبع geo سفارشی یافت نشد" "customGeoErrNotFound" = "منبع geo سفارشی یافت نشد"
"customGeoErrDownload" = "بارگیری ناموفق بود" "customGeoErrDownload" = "بارگیری ناموفق بود"
"customGeoErrUpdateAllIncomplete" = "به‌روزرسانی یک یا چند منبع geo سفارشی ناموفق بود" "customGeoErrUpdateAllIncomplete" = "به‌روزرسانی یک یا چند منبع geo سفارشی ناموفق بود"
"customGeoEmpty" = "هنوز منبع geo سفارشی‌ای ثبت نشده — برای ایجاد روی «افزودن» کلیک کنید"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "کل ترافیک" "allTimeTraffic" = "کل ترافیک"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Sumber geo kustom tidak ditemukan" "customGeoErrNotFound" = "Sumber geo kustom tidak ditemukan"
"customGeoErrDownload" = "Unduh gagal" "customGeoErrDownload" = "Unduh gagal"
"customGeoErrUpdateAllIncomplete" = "Satu atau lebih sumber geo kustom gagal diperbarui" "customGeoErrUpdateAllIncomplete" = "Satu atau lebih sumber geo kustom gagal diperbarui"
"customGeoEmpty" = "Belum ada sumber geo kustom — klik Tambah untuk membuatnya"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Total Lalu Lintas" "allTimeTraffic" = "Total Lalu Lintas"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "カスタム geo ソースが見つかりません" "customGeoErrNotFound" = "カスタム geo ソースが見つかりません"
"customGeoErrDownload" = "ダウンロードに失敗しました" "customGeoErrDownload" = "ダウンロードに失敗しました"
"customGeoErrUpdateAllIncomplete" = "カスタム geo ソースの 1 件以上を更新できませんでした" "customGeoErrUpdateAllIncomplete" = "カスタム geo ソースの 1 件以上を更新できませんでした"
"customGeoEmpty" = "カスタム geo ソースはまだありません — 「追加」をクリックして作成してください"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "総トラフィック" "allTimeTraffic" = "総トラフィック"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Fonte geo personalizada não encontrada" "customGeoErrNotFound" = "Fonte geo personalizada não encontrada"
"customGeoErrDownload" = "Falha no download" "customGeoErrDownload" = "Falha no download"
"customGeoErrUpdateAllIncomplete" = "Falha ao atualizar uma ou mais fontes geo personalizadas" "customGeoErrUpdateAllIncomplete" = "Falha ao atualizar uma ou mais fontes geo personalizadas"
"customGeoEmpty" = "Ainda não há fontes geo personalizadas — clique em Adicionar para criar uma"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tráfego Total" "allTimeTraffic" = "Tráfego Total"

View file

@ -204,6 +204,7 @@
"customGeoErrNotFound" = "Источник не найден" "customGeoErrNotFound" = "Источник не найден"
"customGeoErrDownload" = "Ошибка загрузки" "customGeoErrDownload" = "Ошибка загрузки"
"customGeoErrUpdateAllIncomplete" = "Не удалось обновить один или несколько пользовательских источников" "customGeoErrUpdateAllIncomplete" = "Не удалось обновить один или несколько пользовательских источников"
"customGeoEmpty" = "Пользовательских источников geo пока нет — нажмите «Добавить», чтобы создать"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу" "dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал" "logs" = "Журнал"
"config" = "Конфигурация" "config" = "Конфигурация"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Özel geo kaynağı bulunamadı" "customGeoErrNotFound" = "Özel geo kaynağı bulunamadı"
"customGeoErrDownload" = "İndirme başarısız" "customGeoErrDownload" = "İndirme başarısız"
"customGeoErrUpdateAllIncomplete" = "Bir veya daha fazla özel geo kaynağı güncellenemedi" "customGeoErrUpdateAllIncomplete" = "Bir veya daha fazla özel geo kaynağı güncellenemedi"
"customGeoEmpty" = "Henüz özel geo kaynağı yok — oluşturmak için Ekle'ye tıklayın"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Toplam Trafik" "allTimeTraffic" = "Toplam Trafik"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Джерело geo не знайдено" "customGeoErrNotFound" = "Джерело geo не знайдено"
"customGeoErrDownload" = "Помилка завантаження" "customGeoErrDownload" = "Помилка завантаження"
"customGeoErrUpdateAllIncomplete" = "Не вдалося оновити один або кілька користувацьких джерел" "customGeoErrUpdateAllIncomplete" = "Не вдалося оновити один або кілька користувацьких джерел"
"customGeoEmpty" = "Користувацьких джерел geo поки немає — натисніть «Додати», щоб створити"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Загальний трафік" "allTimeTraffic" = "Загальний трафік"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "Không tìm thấy nguồn geo tùy chỉnh" "customGeoErrNotFound" = "Không tìm thấy nguồn geo tùy chỉnh"
"customGeoErrDownload" = "Tải xuống thất bại" "customGeoErrDownload" = "Tải xuống thất bại"
"customGeoErrUpdateAllIncomplete" = "Một hoặc nhiều nguồn geo tùy chỉnh không cập nhật được" "customGeoErrUpdateAllIncomplete" = "Một hoặc nhiều nguồn geo tùy chỉnh không cập nhật được"
"customGeoEmpty" = "Chưa có nguồn geo tùy chỉnh nào — nhấp Thêm để tạo"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tổng Lưu Lượng" "allTimeTraffic" = "Tổng Lưu Lượng"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "未找到自定义 geo 源" "customGeoErrNotFound" = "未找到自定义 geo 源"
"customGeoErrDownload" = "下载失败" "customGeoErrDownload" = "下载失败"
"customGeoErrUpdateAllIncomplete" = "有一个或多个自定义 geo 源更新失败" "customGeoErrUpdateAllIncomplete" = "有一个或多个自定义 geo 源更新失败"
"customGeoEmpty" = "暂无自定义 geo 源 — 点击「添加」以创建"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "累计总流量" "allTimeTraffic" = "累计总流量"

View file

@ -218,6 +218,7 @@
"customGeoErrNotFound" = "找不到自訂 geo 來源" "customGeoErrNotFound" = "找不到自訂 geo 來源"
"customGeoErrDownload" = "下載失敗" "customGeoErrDownload" = "下載失敗"
"customGeoErrUpdateAllIncomplete" = "有一個或多個自訂 geo 來源更新失敗" "customGeoErrUpdateAllIncomplete" = "有一個或多個自訂 geo 來源更新失敗"
"customGeoEmpty" = "尚無自訂 geo 來源 — 點擊「新增」以建立"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "累計總流量" "allTimeTraffic" = "累計總流量"