3 Commits 3d1d75d65a ... 237b7c898d

Author SHA1 Message Date
  MHSanaei 237b7c898d Bump frontend deps: vue and vite 19 hours ago
  MHSanaei 7368359924 fix(xray): resolve relative log paths under panel log folder 20 hours ago
  MHSanaei f2f5d584b3 fix(frontend): stack form fields on mobile in client/inbound/node modals 21 hours ago

+ 6 - 6
frontend/package-lock.json

@@ -17,7 +17,7 @@
         "dayjs": "^1.11.20",
         "otpauth": "^9.5.1",
         "qs": "^6.13.1",
-        "vue": "^3.5.13",
+        "vue": "^3.5.34",
         "vue-i18n": "^11.1.4",
         "vue3-persian-datetime-picker": "^1.2.2"
       },
@@ -27,7 +27,7 @@
         "eslint": "^10.3.0",
         "eslint-plugin-vue": "^10.9.1",
         "globals": "^17.6.0",
-        "vite": "^8.0.11",
+        "vite": "8.0.13",
         "vue-eslint-parser": "^10.4.0"
       },
       "engines": {
@@ -2623,9 +2623,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.5.14",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
-      "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+      "version": "8.5.15",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+      "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
       "funding": [
         {
           "type": "opencollective",
@@ -2642,7 +2642,7 @@
       ],
       "license": "MIT",
       "dependencies": {
-        "nanoid": "^3.3.11",
+        "nanoid": "^3.3.12",
         "picocolors": "^1.1.1",
         "source-map-js": "^1.2.1"
       },

+ 2 - 2
frontend/package.json

@@ -24,7 +24,7 @@
     "dayjs": "^1.11.20",
     "otpauth": "^9.5.1",
     "qs": "^6.13.1",
-    "vue": "^3.5.13",
+    "vue": "^3.5.34",
     "vue-i18n": "^11.1.4",
     "vue3-persian-datetime-picker": "^1.2.2"
   },
@@ -34,7 +34,7 @@
     "eslint": "^10.3.0",
     "eslint-plugin-vue": "^10.9.1",
     "globals": "^17.6.0",
-    "vite": "^8.0.11",
+    "vite": "8.0.13",
     "vue-eslint-parser": "^10.4.0"
   },
   "overrides": {

+ 13 - 13
frontend/src/pages/clients/ClientFormModal.vue

@@ -270,7 +270,7 @@ async function onSubmit() {
     :ok-button-props="{ loading: submitting }" :width="720" @ok="onSubmit" @cancel="close">
     <a-form layout="vertical" :model="form">
       <a-row :gutter="16">
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.email')" required>
             <a-input-group compact style="display: flex">
               <a-input v-model:value="form.email" :placeholder="t('pages.clients.email')" style="flex: 1" />
@@ -278,7 +278,7 @@ async function onSubmit() {
             </a-input-group>
           </a-form-item>
         </a-col>
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.subId')">
             <a-input-group compact style="display: flex">
               <a-input v-model:value="form.subId" style="flex: 1" />
@@ -289,7 +289,7 @@ async function onSubmit() {
       </a-row>
 
       <a-row :gutter="16">
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.hysteriaAuth')">
             <a-input-group compact style="display: flex">
               <a-input v-model:value="form.auth" style="flex: 1" />
@@ -297,7 +297,7 @@ async function onSubmit() {
             </a-input-group>
           </a-form-item>
         </a-col>
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.password')">
             <a-input-group compact style="display: flex">
               <a-input v-model:value="form.password" style="flex: 1" />
@@ -308,7 +308,7 @@ async function onSubmit() {
       </a-row>
 
       <a-row :gutter="16">
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.uuid')">
             <a-input-group compact style="display: flex">
               <a-input v-model:value="form.uuid" style="flex: 1" />
@@ -316,12 +316,12 @@ async function onSubmit() {
             </a-input-group>
           </a-form-item>
         </a-col>
-        <a-col :span="ipLimitEnable ? 8 : 12">
+        <a-col :xs="24" :md="ipLimitEnable ? 8 : 12">
           <a-form-item :label="t('pages.clients.totalGB')">
             <a-input-number v-model:value="form.totalGB" :min="0" :step="0.1" style="width: 100%" />
           </a-form-item>
         </a-col>
-        <a-col v-if="ipLimitEnable" :span="4">
+        <a-col v-if="ipLimitEnable" :xs="24" :md="4">
           <a-form-item :label="t('pages.clients.limitIp')">
             <a-input-number v-model:value="form.limitIp" :min="0" style="width: 100%" />
           </a-form-item>
@@ -329,7 +329,7 @@ async function onSubmit() {
       </a-row>
 
       <a-row :gutter="16">
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item v-if="form.delayedStart" :label="t('pages.clients.expireDays')">
             <a-input-number v-model:value="form.delayedDays" :min="0" style="width: 100%" />
           </a-form-item>
@@ -337,7 +337,7 @@ async function onSubmit() {
             <a-date-picker v-model:value="form.expiryDate" show-time style="width: 100%" />
           </a-form-item>
         </a-col>
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.delayedStart')">
             <a-switch v-model:checked="form.delayedStart" @change="onDelayedStartToggle" />
           </a-form-item>
@@ -345,7 +345,7 @@ async function onSubmit() {
       </a-row>
 
       <a-row v-if="showFlow || showReverseTag" :gutter="16">
-        <a-col v-if="showFlow" :span="12">
+        <a-col v-if="showFlow" :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.flow')">
             <a-select v-model:value="form.flow">
               <a-select-option value="">{{ t('none') }}</a-select-option>
@@ -353,7 +353,7 @@ async function onSubmit() {
             </a-select>
           </a-form-item>
         </a-col>
-        <a-col v-if="showReverseTag" :span="12">
+        <a-col v-if="showReverseTag" :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.reverseTag')">
             <a-input v-model:value="form.reverseTag" :placeholder="t('pages.clients.reverseTagPlaceholder')" />
           </a-form-item>
@@ -361,13 +361,13 @@ async function onSubmit() {
       </a-row>
 
       <a-row :gutter="16">
-        <a-col v-if="tgBotEnable" :span="12">
+        <a-col v-if="tgBotEnable" :xs="24" :md="12">
           <a-form-item :label="t('pages.clients.telegramId')">
             <a-input-number v-model:value="form.tgId" :min="0" :controls="false"
               :placeholder="t('pages.clients.telegramIdPlaceholder')" style="width: 100%" />
           </a-form-item>
         </a-col>
-        <a-col :span="tgBotEnable ? 12 : 24">
+        <a-col :xs="24" :md="tgBotEnable ? 12 : 24">
           <a-form-item :label="t('pages.clients.comment')">
             <a-input v-model:value="form.comment" />
           </a-form-item>

+ 4 - 4
frontend/src/pages/inbounds/InboundFormModal.vue

@@ -1030,16 +1030,16 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
               </a-col>
             </a-row>
             <a-row v-if="isFallbackEditing(record.rowKey)" :gutter="8" style="margin-top: 8px">
-              <a-col :span="8">
+              <a-col :xs="24" :md="8">
                 <a-input v-model:value="record.name" addon-before="SNI" :placeholder="t('pages.inbounds.fallbacks.matchAny') || 'any'" />
               </a-col>
-              <a-col :span="5">
+              <a-col :xs="24" :md="5">
                 <a-input v-model:value="record.alpn" addon-before="ALPN" :placeholder="t('pages.inbounds.fallbacks.matchAny') || 'any'" />
               </a-col>
-              <a-col :span="7">
+              <a-col :xs="24" :md="7">
                 <a-input v-model:value="record.path" addon-before="Path" placeholder="/" />
               </a-col>
-              <a-col :span="4">
+              <a-col :xs="24" :md="4">
                 <a-input-number v-model:value="record.xver" addon-before="xver" :min="0" :max="2" style="width: 100%" />
               </a-col>
             </a-row>

+ 7 - 7
frontend/src/pages/nodes/NodeFormModal.vue

@@ -117,12 +117,12 @@ async function onSave() {
     :mask-closable="false" width="640px" @ok="onSave" @cancel="close">
     <a-form layout="vertical" :model="form">
       <a-row :gutter="16">
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.nodes.name')" required>
             <a-input v-model:value="form.name" :placeholder="t('pages.nodes.namePlaceholder')" />
           </a-form-item>
         </a-col>
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.nodes.remark')">
             <a-input v-model:value="form.remark" />
           </a-form-item>
@@ -130,7 +130,7 @@ async function onSave() {
       </a-row>
 
       <a-row :gutter="16">
-        <a-col :span="6">
+        <a-col :xs="24" :md="6">
           <a-form-item :label="t('pages.nodes.scheme')">
             <a-select v-model:value="form.scheme">
               <a-select-option value="https">https</a-select-option>
@@ -138,12 +138,12 @@ async function onSave() {
             </a-select>
           </a-form-item>
         </a-col>
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.nodes.address')" required>
             <a-input v-model:value="form.address" :placeholder="t('pages.nodes.addressPlaceholder')" />
           </a-form-item>
         </a-col>
-        <a-col :span="6">
+        <a-col :xs="24" :md="6">
           <a-form-item :label="t('pages.nodes.port')" required>
             <a-input-number v-model:value="form.port" :min="1" :max="65535" style="width: 100%" />
           </a-form-item>
@@ -151,12 +151,12 @@ async function onSave() {
       </a-row>
 
       <a-row :gutter="16">
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.nodes.basePath')">
             <a-input v-model:value="form.basePath" placeholder="/" />
           </a-form-item>
         </a-col>
-        <a-col :span="12">
+        <a-col :xs="24" :md="12">
           <a-form-item :label="t('pages.nodes.enable')">
             <a-switch v-model:checked="form.enable" />
           </a-form-item>

+ 54 - 0
web/service/xray.go

@@ -3,12 +3,15 @@ package service
 import (
 	"encoding/json"
 	"errors"
+	"path/filepath"
 	"runtime"
 	"strings"
 	"sync"
 
+	"github.com/mhsanaei/3x-ui/v3/config"
 	"github.com/mhsanaei/3x-ui/v3/database/model"
 	"github.com/mhsanaei/3x-ui/v3/logger"
+	"github.com/mhsanaei/3x-ui/v3/util/json_util"
 	"github.com/mhsanaei/3x-ui/v3/xray"
 
 	"go.uber.org/atomic"
@@ -104,6 +107,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 	if err != nil {
 		return nil, err
 	}
+	xrayConfig.LogConfig = resolveXrayLogPaths(xrayConfig.LogConfig)
 
 	_, _, _ = s.inboundService.AddTraffic(nil, nil)
 
@@ -253,6 +257,56 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 	return xrayConfig, nil
 }
 
+// resolveXrayLogPaths rewrites relative `log.access` / `log.error` values to
+// absolute paths under config.GetLogFolder(), so Xray writes those files
+// alongside the panel's other logs regardless of the working directory the
+// panel was launched from. Values that are empty, "none", or already absolute
+// are left untouched, as are unparseable log blocks.
+func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
+	if len(logCfg) == 0 {
+		return logCfg
+	}
+	var parsed map[string]any
+	if err := json.Unmarshal(logCfg, &parsed); err != nil {
+		return logCfg
+	}
+	changed := false
+	for _, key := range []string{"access", "error"} {
+		v, ok := parsed[key].(string)
+		if !ok {
+			continue
+		}
+		trimmed := strings.TrimSpace(v)
+		if trimmed == "" || strings.EqualFold(trimmed, "none") {
+			continue
+		}
+		if filepath.IsAbs(trimmed) {
+			continue
+		}
+		cleaned := filepath.ToSlash(filepath.Clean(trimmed))
+		base := filepath.Base(cleaned)
+		if base == "" || base == "." || base == string(filepath.Separator) {
+			continue
+		}
+		// Only rewrite bare names ("./access.log", "access.log").
+		// A nested relative path like "./logs/foo.log" is treated as
+		// a deliberate user choice and left alone.
+		if cleaned != base {
+			continue
+		}
+		parsed[key] = filepath.Join(config.GetLogFolder(), base)
+		changed = true
+	}
+	if !changed {
+		return logCfg
+	}
+	out, err := json.Marshal(parsed)
+	if err != nil {
+		return logCfg
+	}
+	return out
+}
+
 // healShadowsocksClientMethods is the same idea as applyShadowsocksClientMethod
 // (see client.go) but applied at xray-config-build time, to backfill the
 // per-client method field for legacy shadowsocks inbounds whose clients were