Browse Source

Redesign logview UI to match system_setting design language

Replace old Semantic UI accordion layout with the same two-panel
flex design, CSS custom-property theming (light/dark), font stack,
and nav-item styles used by system_setting/main.css.

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53

Replace async locale fetch with inline translation table

$.getJSON path resolution differed between embedded and floating-window
contexts, causing silent failures. Embed all 6 language strings directly
in the script so translation is synchronous and context-independent.
Includes prefix fallback so 'ja' matches 'ja-jp'.

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53

Fix localization and add auto-refresh to logview

Localization: replace applocale dependency with a self-contained
$.getJSON fetch of syslog.json that works in both embedded (system
settings) and standalone (floating window) contexts. Includes a
language prefix fallback so 'ja' matches 'ja-jp'.

Auto-refresh: poll the open log file every 3 seconds and append only
new lines with a brief green highlight animation. A pulsing 'Live'
badge shows in the toolbar while refresh is active. Refresh stops
cleanly when switching files or navigating away (MutationObserver).

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53

Fix log box display mode causing garbled horizontal layout

Changed box.style.display from 'flex' to 'block' when showing the
log content — 'flex' was turning #lv-log-box into a flex container
and laying all line spans out horizontally instead of vertically.

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53

Trim syslog locale to the 6 supported languages

Remove fr-fr, de-de, es-es, pt-br, ru-ru, it-it. Keep only
zh-tw, zh-hk, zh-cn, en-us, ja-jp, ko-kr to match the set
supported by the rest of the system settings.

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53

Fix log coloring and add i18n support to logview

Color fix: scope all token color rules under #lv-log-box and add
!important to beat body.dark span { color: ... !important } in main.css,
which was washing out all span colors in dark mode.

Also fix the regex to normalize \r\n line endings before splitting and
remove the multiline ^ anchor so it correctly matches every log line.

Localization: add locale attributes to all static strings and load
../locale/system_settings/syslog.json via applocale.init(). The new
locale file covers 12 languages: zh-tw, zh-hk, zh-cn, en-us, ja-jp,
ko-kr, fr-fr, de-de, es-es, pt-br, ru-ru, it-it.

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53

Fix logview embedding and add log-line syntax coloring

- Zero out #detail-inner padding for System Log in main.js (same
  pattern as Performance tab) so the two-panel layout fills the pane
- Scope all CSS to #logview-root; remove html/body rules that bled
  into the parent document
- Replace textarea with a div renderer that colorizes each log line:
  date (muted blue-gray), time (muted), source (italic), INFO (green),
  WARN (amber), ERROR/FATAL (red), DEBUG (purple)
- Regex parser handles the pipe-delimited arozos log format

https://claude.ai/code/session_01GWVDJt5UdYpWFqc3YMRd53
Claude 2 tuần trước cách đây
mục cha
commit
8f7ba4e799

+ 638 - 120
src/web/SystemAO/advance/logview.html

@@ -1,143 +1,661 @@
-<!DOCTYPE html>
-<html ng-app="App">
-<head>
-    <title>System Logs</title>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
-    <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
-    <script type="text/javascript" src="../../script/jquery.min.js"></script>
-    <script type="text/javascript" src="../../script/semantic/semantic.min.js"></script>
-    <style>
-        .clickable{
-            cursor: pointer;
-        }
-        .clickable:hover{
-            opacity: 0.7;
-        }
-        .logfile{
-            padding-left: 1em !important;
-            position: relative;
-            padding-right: 1em !important;
-        }
+<!-- logview.html — loaded via $.load() into #detail-inner (padding zeroed by main.js) -->
+<style>
+#logview-root {
+    display: flex;
+    height: 100vh;
+    overflow: hidden;
+    font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
+    font-size: 14px;
+    color: var(--text, #202020);
+    background: var(--bg, #f3f3f3);
+}
 
-        .loglist{
-            background-color: rgb(250, 250, 250);
-        }
+/* ── File-list sidebar ── */
+#lv-sidebar {
+    width: 220px;
+    min-width: 220px;
+    background: var(--sidebar-bg, #ebebeb);
+    border-right: 1px solid var(--sidebar-border, #dcdcdc);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
 
-        .logfile .showing{
-            position: absolute;
-            top: 0.18em;
-            right: 0em;
-            margin-right: -0.4em;
-            opacity: 0;
-        }
+#lv-sidebar-head {
+    padding: 18px 16px 10px 18px;
+    flex-shrink: 0;
+}
 
-        .logfile.active .showing{
-            opacity: 1;
-        }
+#lv-sidebar-head h2 {
+    font-size: 17px;
+    font-weight: 600;
+    letter-spacing: -0.2px;
+    color: var(--title-color, #1a1a1a);
+    margin: 0 0 2px;
+}
 
-        #logrender{
-            width: 100% !important;
-            height: calc(100% - 1.2em);
-            min-height: 50vh;
-            border: 1px solid rgb(231, 231, 231);
-            font-family: monospace;  
-        }
-    </style>
-</head>
-
-<body>
-    <div class="ui container">
-        <div class="ui stackable grid">
-            <div class="four wide column loglist">
-                <h3 class="ui header" style="padding-top: 1em;">
-                    <div class="content">
-                        Log View
-                        <div class="sub header">Check System Log in Real Time</div>
-                    </div>
-                </h3>
-                <div class="ui divider"></div>
-                <div id="logList" class="ui accordion">
-                    
-                </div>
-                <div class="ui divider"></div>
-                <small>Notes: Some log file might be huge. Make sure you have checked the log file size before opening</small>
+#lv-sidebar-head p {
+    font-size: 11.5px;
+    color: var(--text-muted, #888);
+    margin: 0;
+}
+
+#lv-sidebar-divider {
+    height: 1px;
+    background: var(--sidebar-border, #dcdcdc);
+    margin: 6px 0 2px;
+    flex-shrink: 0;
+}
+
+#lv-file-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 4px 8px 8px;
+}
+
+#lv-file-list::-webkit-scrollbar { width: 3px; }
+#lv-file-list::-webkit-scrollbar-thumb {
+    background: var(--scrollbar-thumb, #c8c8c8);
+    border-radius: 2px;
+}
+
+.lv-group { margin-bottom: 2px; }
+
+.lv-group-title {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 4px 8px;
+    font-size: 10.5px;
+    font-weight: 600;
+    letter-spacing: 0.5px;
+    text-transform: uppercase;
+    color: var(--text-muted, #888);
+    cursor: pointer;
+    user-select: none;
+    border-radius: 5px;
+    transition: background 0.08s;
+}
+
+.lv-group-title:hover { background: var(--nav-hover, rgba(0,0,0,0.055)); }
+
+.lv-group-title svg {
+    width: 11px;
+    height: 11px;
+    flex-shrink: 0;
+    transition: transform 0.15s;
+}
+
+.lv-group.collapsed .lv-group-title svg { transform: rotate(-90deg); }
+.lv-group.collapsed .lv-group-items    { display: none; }
+
+.lv-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 5px 10px;
+    border-radius: 6px;
+    cursor: pointer;
+    user-select: none;
+    transition: background 0.08s;
+    font-size: 12.5px;
+    color: var(--text, #202020);
+    margin-bottom: 1px;
+}
+
+.lv-item:hover  { background: var(--nav-hover, rgba(0,0,0,0.055)); }
+.lv-item.active { background: var(--nav-active, rgba(0,0,0,0.09)); font-weight: 500; }
+
+.lv-item svg {
+    width: 13px;
+    height: 13px;
+    flex-shrink: 0;
+    opacity: 0.45;
+}
+
+.lv-item-name {
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.lv-item-size {
+    font-size: 10.5px;
+    color: var(--text-muted, #888);
+    flex-shrink: 0;
+}
+
+.lv-dot {
+    width: 5px;
+    height: 5px;
+    border-radius: 50%;
+    background: #4caf8e;
+    flex-shrink: 0;
+    display: none;
+}
+
+.lv-item.active .lv-dot { display: block; }
+
+#lv-note {
+    padding: 8px 12px 12px;
+    font-size: 11px;
+    color: var(--text-muted, #888);
+    line-height: 1.5;
+    border-top: 1px solid var(--sidebar-border, #dcdcdc);
+    flex-shrink: 0;
+}
+
+/* ── Log viewer pane ── */
+#lv-main {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    background: var(--bg, #f3f3f3);
+}
+
+#lv-toolbar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 18px;
+    border-bottom: 1px solid var(--sidebar-border, #dcdcdc);
+    flex-shrink: 0;
+    gap: 10px;
+}
+
+#lv-toolbar-left {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    overflow: hidden;
+    flex: 1;
+}
+
+#lv-toolbar-title {
+    font-size: 12.5px;
+    font-weight: 500;
+    color: var(--title-color, #1a1a1a);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    display: none;
+}
+
+#lv-hint {
+    font-size: 12.5px;
+    color: var(--text-muted, #888);
+}
+
+/* Live indicator */
+#lv-live-badge {
+    display: none;
+    align-items: center;
+    gap: 4px;
+    font-size: 10.5px;
+    font-weight: 600;
+    color: #4caf8e;
+    flex-shrink: 0;
+}
+
+#lv-live-dot {
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background: #4caf8e;
+    animation: lv-pulse 1.4s ease-in-out infinite;
+}
+
+@keyframes lv-pulse {
+    0%, 100% { opacity: 1; }
+    50%       { opacity: 0.3; }
+}
+
+#lv-toolbar-right {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex-shrink: 0;
+}
+
+#lv-btn-newtab {
+    display: none;
+    align-items: center;
+    gap: 5px;
+    padding: 4px 10px;
+    font-size: 11.5px;
+    font-family: inherit;
+    background: var(--card-bg, #fff);
+    border: 1px solid var(--card-border, #e5e5e5);
+    border-radius: 6px;
+    color: var(--text-dim, #555);
+    cursor: pointer;
+    text-decoration: none;
+    white-space: nowrap;
+    transition: background 0.08s, border-color 0.08s;
+}
+
+#lv-btn-newtab:hover {
+    background: var(--nav-hover, rgba(0,0,0,0.055));
+    border-color: var(--scrollbar-thumb, #c8c8c8);
+}
+
+#lv-btn-newtab svg { width: 12px; height: 12px; opacity: 0.65; }
+
+#lv-content {
+    flex: 1;
+    padding: 14px 16px;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+/* Empty state */
+#lv-empty {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    color: var(--text-muted, #888);
+    user-select: none;
+    pointer-events: none;
+}
+
+#lv-empty svg { width: 32px; height: 32px; opacity: 0.28; }
+#lv-empty p   { font-size: 12.5px; margin: 0; }
+
+/* ── Colored log renderer ── */
+#lv-log-box {
+    display: none;
+    flex: 1;
+    overflow: auto;
+    background: var(--card-bg, #ffffff);
+    border: 1px solid var(--card-border, #e5e5e5);
+    border-radius: 8px;
+    padding: 12px 14px;
+    font-family: 'Cascadia Code', 'Fira Code', 'Consolas', 'Menlo', monospace;
+    font-size: 12px;
+    line-height: 1.7;
+}
+
+body.dark #lv-log-box {
+    background: #1a1a1a !important;
+    border-color: var(--card-border, #3a3a3a) !important;
+}
+
+#lv-log-box::-webkit-scrollbar { width: 5px; height: 5px; }
+#lv-log-box::-webkit-scrollbar-thumb {
+    background: var(--scrollbar-thumb, #c8c8c8);
+    border-radius: 3px;
+}
+
+/* Log token colors — !important beats body.dark span { color: ... !important } in main.css */
+#lv-log-box .ll-line  { display: block; white-space: pre-wrap; word-break: break-all; }
+#lv-log-box .ll-date  { color: #6a8a9a !important; }
+#lv-log-box .ll-time  { color: #7898b0 !important; }
+#lv-log-box .ll-src   { color: #888 !important; font-style: italic; }
+#lv-log-box .ll-info  { color: #4caf8e !important; font-weight: 600; }
+#lv-log-box .ll-warn  { color: #d4a017 !important; font-weight: 600; }
+#lv-log-box .ll-error { color: #e05555 !important; font-weight: 600; }
+#lv-log-box .ll-debug { color: #8080c0 !important; font-weight: 600; }
+#lv-log-box .ll-msg   { color: #202020 !important; }
+
+body.dark #lv-log-box .ll-date  { color: #4a6878 !important; }
+body.dark #lv-log-box .ll-time  { color: #4a6890 !important; }
+body.dark #lv-log-box .ll-src   { color: #606060 !important; }
+body.dark #lv-log-box .ll-msg   { color: #c8c8c8 !important; }
+
+/* New-line highlight on auto-refresh */
+@keyframes lv-new-line {
+    from { background: rgba(76, 175, 142, 0.18); }
+    to   { background: transparent; }
+}
+
+#lv-log-box .ll-new { animation: lv-new-line 1.2s ease-out forwards; }
+</style>
+
+<div id="logview-root">
+
+    <!-- Left: file list -->
+    <div id="lv-sidebar">
+        <div id="lv-sidebar-head">
+            <h2 locale="syslog/title">Log View</h2>
+            <p locale="syslog/subtitle">Check system logs in real time</p>
+        </div>
+        <div id="lv-sidebar-divider"></div>
+        <div id="lv-file-list"></div>
+        <div id="lv-note" locale="syslog/size_warning">Log files may be large — check the size before opening.</div>
+    </div>
+
+    <!-- Right: viewer -->
+    <div id="lv-main">
+        <div id="lv-toolbar">
+            <div id="lv-toolbar-left">
+                <span id="lv-hint" locale="syslog/select_hint">← Select a log file to begin</span>
+                <span id="lv-toolbar-title"></span>
+                <span id="lv-live-badge">
+                    <span id="lv-live-dot"></span>
+                    <span locale="syslog/live">Live</span>
+                </span>
+            </div>
+            <div id="lv-toolbar-right">
+                <a id="lv-btn-newtab" href="#" onclick="lvOpenNewTab(); return false;">
+                    <svg viewBox="0 0 16 16" fill="none">
+                        <path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3M10 2h4m0 0v4m0-4L7 9"
+                              stroke="currentColor" stroke-width="1.4"
+                              stroke-linecap="round" stroke-linejoin="round"/>
+                    </svg>
+                    <span locale="syslog/open_new_tab">Open in New Tab</span>
+                </a>
             </div>
-            <div class="twelve wide column">
-                <textarea id="logrender" spellcheck="false" readonly="true">
-← Pick a log file from the left menu to start debugging
-                </textarea>
-                <a href="#" onclick="openLogInNewTab();">Open In New Tab</a>
-                <br><br>
+        </div>
+        <div id="lv-content">
+            <div id="lv-empty">
+                <svg viewBox="0 0 24 24" fill="none">
+                    <path d="M9 12h6M9 16h4M6 3H4a1 1 0 00-1 1v16a1 1 0 001 1h16a1 1 0 001-1V8l-5-5H6z"
+                          stroke="currentColor" stroke-width="1.5"
+                          stroke-linecap="round" stroke-linejoin="round"/>
+                </svg>
+                <p id="lv-empty-msg" locale="syslog/no_file_selected">No log file selected</p>
             </div>
+            <div id="lv-log-box" aria-label="Log output"></div>
         </div>
     </div>
-    <div class="ui divider"></div>
-    <br>
-</body>
+
+</div>
+
 <script>
-    var currentOpenedLogURL = "";
+(function () {
+    /* ── State ── */
+    var currentLogURL    = '';
+    var currentCategory  = '';
+    var currentFilename  = '';
+    var currentLineCount = 0;
+    var refreshTimer     = null;
+    var REFRESH_MS       = 3000;
+
+    /* ── Theme ── */
+    if (typeof ao_module_getSystemThemeColor === 'function') {
+        ao_module_getSystemThemeColor(function (c) {
+            document.body.classList.toggle('dark', c !== 'whiteTheme');
+        });
+    }
+    window.desktopThemeChanged = function (theme) {
+        document.body.classList.toggle('dark', theme === 'dark');
+    };
+
+    /* ── Localization (inline — no async fetch, works in all contexts) ── */
+    var LV_I18N = {
+        'zh-tw': {
+            'syslog/title':            '日誌檢視器',
+            'syslog/subtitle':         '即時查看系統日誌',
+            'syslog/select_hint':      '← 請從左側選擇日誌檔案',
+            'syslog/open_new_tab':     '在新分頁開啟',
+            'syslog/size_warning':     '日誌檔案可能很大,開啟前請確認檔案大小。',
+            'syslog/no_file_selected': '尚未選擇日誌檔案',
+            'syslog/live':             '即時',
+            'syslog/loading':          '載入中…'
+        },
+        'zh-hk': {
+            'syslog/title':            '日誌檢視器',
+            'syslog/subtitle':         '即時查看系統日誌',
+            'syslog/select_hint':      '← 請從左側選擇日誌檔案',
+            'syslog/open_new_tab':     '在新分頁開啟',
+            'syslog/size_warning':     '日誌檔案可能很大,開啟前請確認檔案大小。',
+            'syslog/no_file_selected': '尚未選擇日誌檔案',
+            'syslog/live':             '即時',
+            'syslog/loading':          '載入中…'
+        },
+        'zh-cn': {
+            'syslog/title':            '日志查看器',
+            'syslog/subtitle':         '实时查看系统日志',
+            'syslog/select_hint':      '← 请从左侧选择日志文件',
+            'syslog/open_new_tab':     '在新标签页打开',
+            'syslog/size_warning':     '日志文件可能很大,打开前请确认文件大小。',
+            'syslog/no_file_selected': '尚未选择日志文件',
+            'syslog/live':             '实时',
+            'syslog/loading':          '加载中…'
+        },
+        'en-us': {
+            'syslog/title':            'Log View',
+            'syslog/subtitle':         'Check system logs in real time',
+            'syslog/select_hint':      '← Select a log file to begin',
+            'syslog/open_new_tab':     'Open in New Tab',
+            'syslog/size_warning':     'Log files may be large — check the size before opening.',
+            'syslog/no_file_selected': 'No log file selected',
+            'syslog/live':             'Live',
+            'syslog/loading':          'Loading…'
+        },
+        'ja-jp': {
+            'syslog/title':            'ログビューア',
+            'syslog/subtitle':         'システムログをリアルタイムで確認',
+            'syslog/select_hint':      '← 左側からログファイルを選択してください',
+            'syslog/open_new_tab':     '新しいタブで開く',
+            'syslog/size_warning':     'ログファイルは大きい場合があります。開く前にファイルサイズを確認してください。',
+            'syslog/no_file_selected': 'ログファイルが選択されていません',
+            'syslog/live':             'ライブ',
+            'syslog/loading':          '読み込み中…'
+        },
+        'ko-kr': {
+            'syslog/title':            '로그 뷰어',
+            'syslog/subtitle':         '시스템 로그를 실시간으로 확인',
+            'syslog/select_hint':      '← 왼쪽에서 로그 파일을 선택하세요',
+            'syslog/open_new_tab':     '새 탭에서 열기',
+            'syslog/size_warning':     '로그 파일이 클 수 있습니다. 열기 전에 파일 크기를 확인하세요.',
+            'syslog/no_file_selected': '로그 파일이 선택되지 않았습니다',
+            'syslog/live':             '라이브',
+            'syslog/loading':          '로드 중…'
+        }
+    };
 
-    function openLogInNewTab(){
-        if (currentOpenedLogURL != ""){
-            window.open(currentOpenedLogURL);
+    (function applyLocale() {
+        var raw  = (localStorage.getItem('global_language') || navigator.language || 'en').toLowerCase();
+        var strings = LV_I18N[raw];
+        // Prefix fallback: 'ja' → 'ja-jp'
+        if (!strings) {
+            var prefix = raw.split('-')[0];
+            for (var k in LV_I18N) {
+                if (k.indexOf(prefix) === 0) { strings = LV_I18N[k]; break; }
+            }
         }
+        if (!strings) return;
+        document.querySelectorAll('[locale]').forEach(function (el) {
+            var v = strings[el.getAttribute('locale')];
+            if (v != null) el.innerHTML = v;
+        });
+    }());
+
+    /* ── New-tab ── */
+    window.lvOpenNewTab = function () {
+        if (currentLogURL) window.open(currentLogURL);
+    };
+
+    /* ── Log colorizer ──
+       Format: "2006-01-02 15:04:05.000000" + "|" + fmt.Sprintf("%-16s", title) + " [LEVEL]" + message + "\n"
+    */
+    var LINE_RE = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}\.\d+)\|([^[]+)(\[(INFO|WARN(?:ING)?|ERROR|FATAL|DEBUG)\])(.*)/i;
+    var LEVEL_CLASS = { INFO:'ll-info', WARN:'ll-warn', WARNING:'ll-warn', ERROR:'ll-error', FATAL:'ll-error', DEBUG:'ll-debug' };
+
+    function esc(s) {
+        return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
     }
 
-    function openLog(object, catergory, filename){
-        $(".logfile.active").removeClass('active');
-        $(object).addClass("active");
-        currentOpenedLogURL = "../../system/log/read?file=" + filename + "&catergory=" + catergory;
-        $.get(currentOpenedLogURL, function(data){
-            if (data.error !== undefined){
-                alert(data.error);
-                return;
+    function renderLines(lines, extraClass) {
+        var html = '';
+        for (var i = 0; i < lines.length; i++) {
+            var line = lines[i];
+            if (line === '') { html += '<span class="ll-line' + extraClass + '"> </span>'; continue; }
+            var m = LINE_RE.exec(line);
+            if (m) {
+                var lvl = m[5].toUpperCase();
+                html +=
+                    '<span class="ll-line' + extraClass + '">' +
+                        '<span class="ll-date">'  + esc(m[1]) + '</span>' +
+                        ' <span class="ll-time">' + esc(m[2]) + '</span>' +
+                        '<span class="ll-src">|'  + esc(m[3]) + '</span>' +
+                        '<span class="' + (LEVEL_CLASS[lvl] || 'll-info') + '">' + esc(m[4]) + '</span>' +
+                        '<span class="ll-msg">'   + esc(m[6]) + '</span>' +
+                    '</span>';
+            } else {
+                html += '<span class="ll-line' + extraClass + ' ll-msg">' + esc(line) + '</span>';
             }
-            $("#logrender").val(data);
-        }); 
+        }
+        return html;
+    }
+
+    function splitLines(text) {
+        return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
+    }
+
+    /* ── Auto-refresh ── */
+    function stopRefresh() {
+        if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
+        document.getElementById('lv-live-badge').style.display = 'none';
+    }
+
+    function startRefresh() {
+        stopRefresh();
+        document.getElementById('lv-live-badge').style.display = 'flex';
+        refreshTimer = setInterval(function () {
+            if (!currentLogURL) { stopRefresh(); return; }
+            $.get(currentLogURL, function (data) {
+                if (typeof data !== 'string') return;
+                var lines = splitLines(data);
+                // Remove trailing empty line from split
+                while (lines.length && lines[lines.length - 1] === '') lines.pop();
+                if (lines.length <= currentLineCount) return;
+
+                var newLines = lines.slice(currentLineCount);
+                currentLineCount = lines.length;
+
+                var box = document.getElementById('lv-log-box');
+                var wasAtBottom = (box.scrollHeight - box.scrollTop - box.clientHeight) < 40;
+                box.insertAdjacentHTML('beforeend', renderLines(newLines, ' ll-new'));
+                if (wasAtBottom) box.scrollTop = box.scrollHeight;
+            });
+        }, REFRESH_MS);
     }
 
-    function initLogList(){
-        $("#logList").html("");
-        $.get("../../system/log/list", function(data){
-            //console.log(data);
-            for (let [key, value] of Object.entries(data)) {
-                console.log(key, value);
-                value.reverse(); //Default value was from oldest to newest
-                var fileItemList = "";
-                value.forEach(file => {
-                    fileItemList += `<div class="item clickable logfile" onclick="openLog(this, '${key}','${file.Filename}');">
-                            <i class="file outline icon"></i>
-                            <div class="content">
-                                ${file.Title} (${formatBytes(file.Filesize)})
-                                <div class="showing"><i class="green chevron right icon"></i></div>
-                            </div>
-                        </div>`;
-                })
-                $("#logList").append(`<div class="title">
-                    <i class="dropdown icon"></i>
-                        ${key}
-                    </div>
-                    <div class="content">
-                        <div class="ui list">
-                            ${fileItemList}
-                        </div>
-                    </div>`);
+    /* ── Open a log file ── */
+    function openLog(itemEl, category, filename) {
+        stopRefresh();
+        document.querySelectorAll('.lv-item.active').forEach(function (el) { el.classList.remove('active'); });
+        itemEl.classList.add('active');
+
+        currentCategory = category;
+        currentFilename = filename;
+        currentLogURL   = '../../system/log/read?file=' + encodeURIComponent(filename) +
+                          '&catergory=' + encodeURIComponent(category);
+
+        var box   = document.getElementById('lv-log-box');
+        var empty = document.getElementById('lv-empty');
+        var msg   = document.getElementById('lv-empty-msg');
+        var title = document.getElementById('lv-toolbar-title');
+        var hint  = document.getElementById('lv-hint');
+        var btn   = document.getElementById('lv-btn-newtab');
+
+        box.style.display = 'none';
+        empty.style.display = 'flex';
+        msg.textContent = 'Loading…';
+
+        $.get(currentLogURL, function (data) {
+            if (typeof data === 'object' && data.error) {
+                msg.textContent = data.error;
+                return;
             }
+            var text  = typeof data === 'string' ? data : String(data);
+            var lines = splitLines(text);
+            while (lines.length && lines[lines.length - 1] === '') lines.pop();
+            currentLineCount = lines.length;
 
-            $(".ui.accordion").accordion();
+            box.innerHTML = renderLines(lines, '');
+            box.style.display = 'block';
+            empty.style.display = 'none';
+            title.textContent  = filename;
+            title.style.display = '';
+            hint.style.display  = 'none';
+            btn.style.display   = 'flex';
+            box.scrollTop = box.scrollHeight;
+
+            startRefresh();
         });
     }
-    initLogList();
 
-    
-    function formatBytes(x){
-        var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
-        let l = 0, n = parseInt(x, 10) || 0;
-        while(n >= 1024 && ++l){
-            n = n/1024;
-        }
-        return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
+    /* ── Group toggle ── */
+    function toggleGroup(g) { g.classList.toggle('collapsed'); }
+
+    function fmtBytes(x) {
+        var u = ['B','KB','MB','GB'], n = parseInt(x, 10) || 0, l = 0;
+        while (n >= 1024 && ++l) n /= 1024;
+        return (n.toFixed(n < 10 && l > 0 ? 1 : 0)) + ' ' + u[l];
+    }
+
+    /* ── Build file list ── */
+    function initLogList() {
+        var nav = document.getElementById('lv-file-list');
+        nav.innerHTML = '';
+        $.get('../../system/log/list', function (data) {
+            for (var key in data) {
+                if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
+                var files = data[key].slice().reverse();
+
+                var groupEl  = document.createElement('div');
+                groupEl.className = 'lv-group';
+
+                var titleEl = document.createElement('div');
+                titleEl.className = 'lv-group-title';
+                titleEl.innerHTML =
+                    '<svg viewBox="0 0 16 16" fill="none">' +
+                        '<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
+                    '</svg>' + esc(key);
+                (function (g) { titleEl.addEventListener('click', function () { toggleGroup(g); }); }(groupEl));
+
+                var itemsEl = document.createElement('div');
+                itemsEl.className = 'lv-group-items';
+
+                files.forEach(function (file) {
+                    var item = document.createElement('div');
+                    item.className = 'lv-item';
+                    item.innerHTML =
+                        '<svg viewBox="0 0 16 16" fill="none">' +
+                            '<path d="M10 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V5l-3-3z" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>' +
+                            '<path d="M10 2v3h3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>' +
+                        '</svg>' +
+                        '<span class="lv-item-name">' + esc(file.Title) + '</span>' +
+                        '<span class="lv-item-size">' + fmtBytes(file.Filesize) + '</span>' +
+                        '<span class="lv-dot"></span>';
+                    (function (el, k, f) {
+                        el.addEventListener('click', function () { openLog(el, k, f.Filename); });
+                    }(item, key, file));
+                    itemsEl.appendChild(item);
+                });
+
+                groupEl.appendChild(titleEl);
+                groupEl.appendChild(itemsEl);
+                nav.appendChild(groupEl);
+            }
+        });
+    }
+
+    /* Stop refresh when navigating away (detail panel replaced) */
+    var observer = new MutationObserver(function (mutations) {
+        mutations.forEach(function (m) {
+            m.removedNodes.forEach(function (node) {
+                if (node.id === 'logview-root' || (node.querySelector && node.querySelector('#logview-root'))) {
+                    stopRefresh();
+                    observer.disconnect();
+                }
+            });
+        });
+    });
+    var detailInner = document.getElementById('detail-inner');
+    if (detailInner && detailInner.parentNode) {
+        observer.observe(detailInner.parentNode, { childList: true, subtree: true });
     }
+
+    initLogList();
+}());
 </script>
-</html>

+ 107 - 0
src/web/SystemAO/locale/system_settings/syslog.json

@@ -0,0 +1,107 @@
+{
+    "author": "tobychui",
+    "version": "1.0",
+    "keys": {
+        "zh-tw": {
+            "name": "繁體中文(台灣)",
+            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+            "strings": {
+                "syslog/title": "日誌檢視器",
+                "syslog/subtitle": "即時查看系統日誌",
+                "syslog/select_hint": "← 請從左側選擇日誌檔案",
+                "syslog/open_new_tab": "在新分頁開啟",
+                "syslog/size_warning": "日誌檔案可能很大,開啟前請確認檔案大小。",
+                "syslog/no_file_selected": "尚未選擇日誌檔案",
+                "syslog/loading": "載入中…",
+                "syslog/error_prefix": "錯誤:",
+                "": ""
+            },
+            "titles": {},
+            "placeholder": {}
+        },
+        "zh-hk": {
+            "name": "繁體中文(香港)",
+            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+            "strings": {
+                "syslog/title": "日誌檢視器",
+                "syslog/subtitle": "即時查看系統日誌",
+                "syslog/select_hint": "← 請從左側選擇日誌檔案",
+                "syslog/open_new_tab": "在新分頁開啟",
+                "syslog/size_warning": "日誌檔案可能很大,開啟前請確認檔案大小。",
+                "syslog/no_file_selected": "尚未選擇日誌檔案",
+                "syslog/loading": "載入中…",
+                "syslog/error_prefix": "錯誤:",
+                "": ""
+            },
+            "titles": {},
+            "placeholder": {}
+        },
+        "zh-cn": {
+            "name": "简体中文",
+            "strings": {
+                "syslog/title": "日志查看器",
+                "syslog/subtitle": "实时查看系统日志",
+                "syslog/select_hint": "← 请从左侧选择日志文件",
+                "syslog/open_new_tab": "在新标签页打开",
+                "syslog/size_warning": "日志文件可能很大,打开前请确认文件大小。",
+                "syslog/no_file_selected": "尚未选择日志文件",
+                "syslog/loading": "加载中…",
+                "syslog/error_prefix": "错误:",
+                "": ""
+            },
+            "titles": {},
+            "placeholder": {}
+        },
+        "en-us": {
+            "name": "English (US)",
+            "fontFamily": "Arial, Helvetica, sans-serif",
+            "strings": {
+                "syslog/title": "Log View",
+                "syslog/subtitle": "Check system logs in real time",
+                "syslog/select_hint": "← Select a log file to begin",
+                "syslog/open_new_tab": "Open in New Tab",
+                "syslog/size_warning": "Log files may be large — check the size before opening.",
+                "syslog/no_file_selected": "No log file selected",
+                "syslog/loading": "Loading…",
+                "syslog/error_prefix": "Error: ",
+                "": ""
+            },
+            "titles": {},
+            "placeholder": {}
+        },
+        "ja-jp": {
+            "name": "日本語",
+            "fontFamily": "\"Meiryo UI\", \"Arial Unicode MS\", \"Hiragino Kaku Gothic Pro\"",
+            "strings": {
+                "syslog/title": "ログビューア",
+                "syslog/subtitle": "システムログをリアルタイムで確認",
+                "syslog/select_hint": "← 左側からログファイルを選択してください",
+                "syslog/open_new_tab": "新しいタブで開く",
+                "syslog/size_warning": "ログファイルは大きい場合があります。開く前にファイルサイズを確認してください。",
+                "syslog/no_file_selected": "ログファイルが選択されていません",
+                "syslog/loading": "読み込み中…",
+                "syslog/error_prefix": "エラー:",
+                "": ""
+            },
+            "titles": {},
+            "placeholder": {}
+        },
+        "ko-kr": {
+            "name": "한국어",
+            "fontFamily": "\"Malgun Gothic\", \"Apple SD Gothic Neo\"",
+            "strings": {
+                "syslog/title": "로그 뷰어",
+                "syslog/subtitle": "시스템 로그를 실시간으로 확인",
+                "syslog/select_hint": "← 왼쪽에서 로그 파일을 선택하세요",
+                "syslog/open_new_tab": "새 탭에서 열기",
+                "syslog/size_warning": "로그 파일이 클 수 있습니다. 열기 전에 파일 크기를 확인하세요.",
+                "syslog/no_file_selected": "로그 파일이 선택되지 않았습니다",
+                "syslog/loading": "로드 중…",
+                "syslog/error_prefix": "오류: ",
+                "": ""
+            },
+            "titles": {},
+            "placeholder": {}
+        },
+    }
+}

+ 2 - 2
src/web/SystemAO/system_setting/main.js

@@ -153,8 +153,8 @@ function loadContent(moduleInfo) {
     $('#detail-inner').html('<div style="color:#999;font-size:13px;">Loading\u2026</div>');
     $('#detail-inner').load('../../' + moduleInfo.StartDir, function () { injectIME(); });
 
-    // For performance tab, set detail-inner padding to 0 to avoid unnecessary reflow when rendering charts
-    if (moduleInfo.Name === 'Performance') {
+    // Remove padding for full-bleed modules (Performance charts, System Log two-panel layout)
+    if (moduleInfo.Name === 'Performance' || moduleInfo.Name === 'System Log') {
         $('#detail-inner').css('padding', '0');
     } else {
         $('#detail-inner').css('padding', '');