5 Commits 299572a4c2 ... db7e7dcd29

Author SHA1 Message Date
  Tara Rostami db7e7dcd29 css [fixes] (#3487) 1 day ago
  mhsanaei 01b8a27996 bug fix 1 day ago
  mhsanaei 3764ece26c v2.8.1 1 day ago
  Tara Rostami d7efc2aef9 Minor Fixes (#3483) 1 day ago
  fgsfds 2eb8abf61e Improved xray logs display handling (#3475) 1 day ago
6 changed files with 228 additions and 82 deletions
  1. 1 1
      config/version
  2. 0 0
      web/assets/css/custom.min.css
  3. 1 2
      web/html/inbounds.html
  4. 92 61
      web/html/index.html
  5. 77 2
      web/html/login.html
  6. 57 16
      web/service/server.go

+ 1 - 1
config/version

@@ -1 +1 @@
-2.8.0
+2.8.1

File diff suppressed because it is too large
+ 0 - 0
web/assets/css/custom.min.css


+ 1 - 2
web/html/inbounds.html

@@ -568,8 +568,7 @@
                             <tr>
                               <td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
                               <td>
-                                <a-tag color="blue">[[ i18n("pages.inbounds.periodicTrafficReset." +
-                                  dbInbound.trafficReset) ]]</a-tag>
+                                <a-tag color="blue">[[ dbInbound.trafficReset ]]</a-tag>
                               </td>
                             </tr>
                           </table>

+ 92 - 61
web/html/index.html

@@ -39,7 +39,7 @@
                               </template>
                             </a-tooltip>
                             <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
-                              <a-button size="small" type="default" class="ml-8" @click="openCpuHistory()">
+                              <a-button size="small" shape="circle" class="ml-8" @click="openCpuHistory()">
                                 <a-icon type="history" />
                               </a-button>
                             </a-tooltip>
@@ -343,7 +343,7 @@
     <a-form layout="inline">
       <a-form-item class="mr-05">
         <a-input-group compact>
-          <a-select size="small" v-model="logModal.rows" class="w-70" @change="openLogs()"
+          <a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }" @change="openLogs()"
             :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="10">10</a-select-option>
             <a-select-option value="20">20</a-select-option>
@@ -351,7 +351,7 @@
             <a-select-option value="100">100</a-select-option>
             <a-select-option value="500">500</a-select-option>
           </a-select>
-          <a-select size="small" v-model="logModal.level" class="w-95" @change="openLogs()"
+          <a-select size="small" v-model="logModal.level" :style="{ width: '95px' }" @change="openLogs()"
             :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="debug">Debug</a-select-option>
             <a-select-option value="info">Info</a-select-option>
@@ -365,8 +365,7 @@
         <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
       </a-form-item>
       <a-form-item style="float: right;">
-        <a-button type="primary" icon="download"
-          @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
+        <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
       </a-form-item>
     </a-form>
     <div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
@@ -382,7 +381,7 @@
     <a-form layout="inline">
       <a-form-item class="mr-05">
         <a-input-group compact>
-          <a-select size="small" v-model="xraylogModal.rows" class="w-70" @change="openXrayLogs()"
+          <a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }" @change="openXrayLogs()"
             :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="10">10</a-select-option>
             <a-select-option value="20">20</a-select-option>
@@ -401,8 +400,7 @@
         <a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
       </a-form-item>
       <a-form-item style="float: right;">
-        <a-button type="primary" icon="download"
-          @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
+        <a-button type="primary" icon="download" @click="downloadXrayLogs"></a-button>
       </a-form-item>
     </a-form>
     <div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
@@ -431,7 +429,7 @@
     @cancel="() => cpuHistoryModal.visible = false" :class="themeSwitcher.currentTheme" width="900px" footer="">
     <template slot="title">
       CPU History
-      <a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 140px"
+      <a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
         @change="fetchCpuHistoryBucket">
         <a-select-option :value="2">2s</a-select-option>
         <a-select-option :value="30">30s</a-select-option>
@@ -441,7 +439,7 @@
         <a-select-option :value="300">5m</a-select-option>
       </a-select>
     </template>
-    <div style="padding: 8px 0;">
+    <div style="padding:16px">
       <sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
         :stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
         :max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
@@ -466,7 +464,7 @@
       strokeWidth: { type: Number, default: 2 },
       maxPoints: { type: Number, default: 120 },
       showGrid: { type: Boolean, default: true },
-      gridColor: { type: String, default: 'rgba(255,255,255,0.08)' },
+      gridColor: { type: String, default: 'rgba(0,0,0,0.1)' },
       fillOpacity: { type: Number, default: 0.15 },
       showMarker: { type: Boolean, default: true },
       markerRadius: { type: Number, default: 2.8 },
@@ -540,7 +538,7 @@
         const h = this.drawHeight
         const w = this.drawWidth
         // draw at 25%, 50%, 75%
-        return [0.25, 0.5, 0.75]
+        return [0, 0.25, 0.5, 0.75, 1]
           .map(r => Math.round(this.paddingTop + h * r))
           .map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
       },
@@ -606,7 +604,7 @@
       },
     },
     template: `
-      <svg width="100%" :height="height" :viewBox="viewBoxAttr" preserveAspectRatio="none" style="display:block"
+      <svg width="100%" :height="height" :viewBox="viewBoxAttr" preserveAspectRatio="none" class="idx-cpu-history-svg"
            @mousemove="onMouseMove" @mouseleave="onMouseLeave">
         <defs>
           <linearGradient id="spkGrad" x1="0" y1="0" x2="0" y2="1">
@@ -615,16 +613,16 @@
           </linearGradient>
         </defs>
         <g v-if="showGrid">
-          <line v-for="(g,i) in gridLines" :key="i" :x1="g.x1" :y1="g.y1" :x2="g.x2" :y2="g.y2" :stroke="gridColor" stroke-width="1"/>
+          <line v-for="(g,i) in gridLines" :key="i" :x1="g.x1" :y1="g.y1" :x2="g.x2" :y2="g.y2" :stroke="gridColor" stroke-width="1" class="cpu-grid-line" />
         </g>
         <g v-if="showAxes">
           <!-- Y ticks/labels -->
           <g v-for="(t,i) in yTicks" :key="'y'+i">
-            <text :x="Math.max(0, paddingLeft - 4)" :y="t.y + 4" text-anchor="end" font-size="10" fill="rgba(200,200,200,0.8)" v-text="t.label"></text>
+            <text class="cpu-grid-y-text" :x="Math.max(0, paddingLeft - 4)" :y="t.y + 4" text-anchor="end" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
           </g>
           <!-- X ticks/labels -->
           <g v-for="(t,i) in xTicks" :key="'x'+i">
-            <text :x="t.x" :y="paddingTop + drawHeight + 14" text-anchor="middle" font-size="10" fill="rgba(200,200,200,0.8)" v-text="t.label"></text>
+            <text class="cpu-grid-x-text" :x="t.x" :y="paddingTop + drawHeight + 22" text-anchor="middle" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
           </g>
         </g>
         <path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
@@ -632,9 +630,9 @@
         <circle v-if="showMarker && lastPoint" :cx="lastPoint[0]" :cy="lastPoint[1]" :r="markerRadius" :fill="stroke" />
         <!-- Hover marker/tooltip -->
         <g v-if="showTooltip && hoverIdx >= 0">
-          <line :x1="pointsArr[hoverIdx][0]" :x2="pointsArr[hoverIdx][0]" :y1="paddingTop" :y2="paddingTop + drawHeight" stroke="rgba(255,255,255,0.25)" stroke-width="1" />
+          <line class="cpu-grid-h-line" :x1="pointsArr[hoverIdx][0]" :x2="pointsArr[hoverIdx][0]" :y1="paddingTop" :y2="paddingTop + drawHeight" stroke="rgba(0,0,0,0.2)" stroke-width="1" />
           <circle :cx="pointsArr[hoverIdx][0]" :cy="pointsArr[hoverIdx][1]" r="3.5" :fill="stroke" />
-          <text :x="pointsArr[hoverIdx][0]" :y="paddingTop + 12" text-anchor="middle" font-size="11" fill="#fff" style="paint-order: stroke; stroke: rgba(0,0,0,0.35); stroke-width: 3;" v-text="fmtHoverText()"></text>
+          <text class="cpu-grid-text" :x="pointsArr[hoverIdx][0]" :y="paddingTop + 12" text-anchor="middle" font-size="11" fill="rgba(0,0,0,0.8)" v-text="fmtHoverText()"></text>
         </g>
       </svg>
     `,
@@ -796,59 +794,74 @@
   };
 
   const xraylogModal = {
-    visible: false,
-    logs: [],
-    rows: 20,
-    showDirect: true,
-    showBlocked: true,
-    showProxy: true,
-    loading: false,
-    show(logs) {
-      this.visible = true;
-      this.logs = logs;
-      this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
-    },
-    formatLogs(logs) {
-      let formattedLogs = '';
-
-      logs.forEach((log, index) => {
-        if (index > 0) formattedLogs += '<br>';
+      visible: false,
+      logs: [],
+      rows: 20,
+      showDirect: true,
+      showBlocked: true,
+      showProxy: true,
+      loading: false,
+      show(logs) {
+        this.visible = true;
+        this.logs = logs;
+        this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
+      },
+      formatLogs(logs) {
+        let formattedLogs = `
+<style>
+  table {
+    border-collapse: collapse;
+    width: auto;
+  }
 
-        const parts = log.split(' ');
+  table td, table th {
+    padding: 2px 15px;
+  }
+</style>
 
-        if (parts.length === 10) {
-          const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
-          const from = `<b>${parts[3]}</b>`;
-          const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
+<table>
+    <tr>
+        <th>Date</th>
+        <th>From</th>
+        <th>To</th>
+        <th>Inbound</th>
+        <th>Outbound</th>
+        <th>Email</th>
+    </tr>
+`;
 
+        logs.reverse().forEach((log, index) => {
           let outboundColor = '';
-          if (parts[9] === "b") {
+          if (log.Event === 1) {
             outboundColor = ' style="color: #e04141;"'; //red for blocked
           }
-          else if (parts[9] === "p") {
+          else if (log.Event === 2) {
             outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
           }
 
-          formattedLogs += `<span${outboundColor}>
-${dateTime}
- ${parts[2]}
- ${from}
- ${parts[4]}
- ${to}
- ${parts.slice(6, 9).join(' ')}
-</span>`;
-        } else {
-          formattedLogs += `<span>${log}</span>`;
-        }
-      });
+          let text = ``;
+          if (log.Email !== "") {
+            text = `<td>${log.Email}</td>`;
+          }
 
-      return formattedLogs;
-    },
-    hide() {
-      this.visible = false;
-    },
-  };
+          formattedLogs += `
+<tr ${outboundColor}>
+    <td><b>${new Date(log.DateTime).toLocaleString()}</b></td>
+    <td>${log.FromAddress}</td>
+    <td>${log.ToAddress}</td>
+    <td>${log.Inbound}</td>
+    <td>${log.Outbound}</td>
+    ${text}
+</tr>
+`;
+        });
 
+        return formattedLogs += "</table>";
+      },
+      hide() {
+        this.visible = false;
+      },
+    };
   const backupModal = {
     visible: false,
     show() {
@@ -1023,6 +1036,25 @@ ${dateTime}
         await PromiseUtil.sleep(500);
         xraylogModal.loading = false;
       },
+      downloadXrayLogs() {
+        if (!Array.isArray(this.xraylogModal.logs) || this.xraylogModal.logs.length === 0) {
+          FileManager.downloadTextFile('', 'x-ui.log');
+          return;
+        }
+        const lines = this.xraylogModal.logs.map(l => {
+          try {
+            const dt = l.DateTime ? new Date(l.DateTime) : null;
+            const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
+            const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
+            const eventText = eventMap[l.Event] || String(l.Event ?? '');
+            const emailPart = l.Email ? ` Email=${l.Email}` : '';
+            return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
+          } catch (e) {
+            return JSON.stringify(l);
+          }
+        }).join('\n');
+        FileManager.downloadTextFile(lines, 'x-ui.log');
+      },
       async openConfig() {
         this.loading(true);
         const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
@@ -1071,7 +1103,6 @@ ${dateTime}
         fileInput.click();
       },
     },
-    computed: {},
     async mounted() {
       if (window.location.protocol !== "https:") {
         this.showAlert = true;

+ 77 - 2
web/html/login.html

@@ -4,7 +4,7 @@
 {{ template "page/body_start" .}}
 <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
   <transition name="list" appear>
-  <a-layout-content class="under min-h-100vh">
+  <a-layout-content class="under min-h-0">
       <div class="waves-header">
         <div class="waves-inner-header"></div>
         <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -20,7 +20,7 @@
           </g>
         </svg>
       </div>
-  <a-row type="flex" justify="center" align="middle" class="h-100 overflow-hidden-auto">
+  <a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
         <a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
           <template v-if="!loadingStates.fetched">
             <div class="text-center">
@@ -184,5 +184,80 @@
       newWord.classList.add('is-visible');
     }
   });
+
+  const pm_input_selector = 'input.ant-input, textarea.ant-input';
+  const pm_strip_props = [
+    'background',
+    'background-color',
+    'background-image',
+    'color'
+  ];
+
+  const pm_observed_forms = new WeakSet();
+
+  function pm_strip_inline(el) {
+    if (!el || el.nodeType !== 1 || !el.matches?.(pm_input_selector)) return;
+
+    let did_change = false;
+    for (const prop of pm_strip_props) {
+      if (el.style.getPropertyValue(prop)) {
+        el.style.removeProperty(prop);
+        did_change = true;
+      }
+    }
+
+    if (did_change && el.style.length === 0) {
+      el.removeAttribute('style');
+    }
+  }
+
+  function pm_attach_observer(form) {
+    if (pm_observed_forms.has(form)) return;
+    pm_observed_forms.add(form);
+
+    form.querySelectorAll(pm_input_selector).forEach(pm_strip_inline);
+
+    const pm_mo = new MutationObserver(mutations => {
+      for (const m of mutations) {
+        if (m.type === 'attributes') {
+          pm_strip_inline(m.target);
+        } else if (m.type === 'childList') {
+          for (const n of m.addedNodes) {
+            if (n.nodeType !== 1) continue;
+            if (n.matches?.(pm_input_selector)) pm_strip_inline(n);
+            n.querySelectorAll?.(pm_input_selector).forEach(pm_strip_inline);
+          }
+        }
+      }
+    });
+
+    pm_mo.observe(form, {
+      attributes: true,
+      attributeFilter: ['style'],
+      childList: true,
+      subtree: true
+    });
+  }
+
+  function pm_init() {
+    document.querySelectorAll('form.ant-form').forEach(pm_attach_observer);
+    const pm_host = document.getElementById('login') || document.body;
+    const pm_wait_for_forms = new MutationObserver(mutations => {
+      for (const m of mutations) {
+        for (const n of m.addedNodes) {
+          if (n.nodeType !== 1) continue;
+          if (n.matches?.('form.ant-form')) pm_attach_observer(n);
+          n.querySelectorAll?.('form.ant-form').forEach(pm_attach_observer);
+        }
+      }
+    });
+    pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });
+  }
+
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', pm_init, { once: true });
+  } else {
+    pm_init();
+  }
 </script>
 {{ template "page/body_end" .}}

+ 57 - 16
web/service/server.go

@@ -174,6 +174,16 @@ type CPUSample struct {
 	Cpu float64 `json:"cpu"` // percent 0..100
 }
 
+type LogEntry struct {
+	DateTime    time.Time
+	FromAddress string
+	ToAddress   string
+	Inbound     string
+	Outbound    string
+	Email       string
+	Event       int
+}
+
 func getPublicIP(url string) string {
 	client := &http.Client{
 		Timeout: 3 * time.Second,
@@ -704,19 +714,25 @@ func (s *ServerService) GetXrayLogs(
 	showBlocked string,
 	showProxy string,
 	freedoms []string,
-	blackholes []string) []string {
+	blackholes []string) []LogEntry {
+
+	const (
+		Direct = iota
+		Blocked
+		Proxied
+	)
 
 	countInt, _ := strconv.Atoi(count)
-	var lines []string
+	var entries []LogEntry
 
 	pathToAccessLog, err := xray.GetAccessLogPath()
 	if err != nil {
-		return lines
+		return nil
 	}
 
 	file, err := os.Open(pathToAccessLog)
 	if err != nil {
-		return lines
+		return nil
 	}
 	defer file.Close()
 
@@ -735,37 +751,62 @@ func (s *ServerService) GetXrayLogs(
 			continue
 		}
 
-		//adding suffixes to further distinguish entries by outbound
-		if hasSuffix(line, freedoms) {
+		var entry LogEntry
+		parts := strings.Fields(line)
+
+		for i, part := range parts {
+
+			if i == 0 {
+				dateTime, err := time.Parse("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1])
+				if err != nil {
+					continue
+				}
+				entry.DateTime = dateTime
+			}
+
+			if part == "from" {
+				entry.FromAddress = parts[i+1]
+			} else if part == "accepted" {
+				entry.ToAddress = parts[i+1]
+			} else if strings.HasPrefix(part, "[") {
+				entry.Inbound = part[1:]
+			} else if strings.HasSuffix(part, "]") {
+				entry.Outbound = part[:len(part)-1]
+			} else if part == "email:" {
+				entry.Email = parts[i+1]
+			}
+		}
+
+		if logEntryContains(line, freedoms) {
 			if showDirect == "false" {
 				continue
 			}
-			line = line + " f"
-		} else if hasSuffix(line, blackholes) {
+			entry.Event = Direct
+		} else if logEntryContains(line, blackholes) {
 			if showBlocked == "false" {
 				continue
 			}
-			line = line + " b"
+			entry.Event = Blocked
 		} else {
 			if showProxy == "false" {
 				continue
 			}
-			line = line + " p"
+			entry.Event = Proxied
 		}
 
-		lines = append(lines, line)
+		entries = append(entries, entry)
 	}
 
-	if len(lines) > countInt {
-		lines = lines[len(lines)-countInt:]
+	if len(entries) > countInt {
+		entries = entries[len(entries)-countInt:]
 	}
 
-	return lines
+	return entries
 }
 
-func hasSuffix(line string, suffixes []string) bool {
+func logEntryContains(line string, suffixes []string) bool {
 	for _, sfx := range suffixes {
-		if strings.HasSuffix(line, sfx+"]") {
+		if strings.Contains(line, sfx+"]") {
 			return true
 		}
 	}

Some files were not shown because too many files changed in this diff