From 7a25f7aed27c73c2212ab00f154e77a087ddbe39 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:34:15 +0000 Subject: [PATCH 1/3] Dashboard and Admin Panel Overhaul - Completely remade the dashboard and admin panel UI with a 'Premium' glassmorphism design system. - Refactored frontend logic into a modular App pattern for better maintainability and state management. - Added new features: - Command Control: Enable/disable specific commands per guild. - Ticket System: Configure a support ticket system via the dashboard. - Analytics: Added join/leave trend charts and recent activity feed to the dashboard home. - Global Maintenance Mode: Admins can now put the bot in maintenance mode with a custom reason. - Global Blacklist: Admins can restrict users from using the bot across all guilds. - System Health: Real-time memory and WebSocket ping monitoring for the admin panel. - Real Global User Search: Find users across all guilds the bot is in. - Improved performance by implementing a 60-second in-memory cache for Firestore settings in interactionCreate.js. - Updated maintenance bypass logic to check for Bot Owner IDs. - Added chart.js to dependencies. - Ensured absolute emoji-free UI/logs as per project guidelines. Co-authored-by: systemcmd0122 <155505304+systemcmd0122@users.noreply.github.com> --- events/interactionCreate.js | 71 ++++++ package-lock.json | 31 +-- package.json | 1 + public/admin.html | 9 + public/admin.js | 331 ++++++++++++++++++++------- public/client.js | 438 ++++++++++++++++++++++++++---------- public/dashboard.html | 2 + public/style.css | 31 ++- src/routes/admin.js | 91 ++++++++ src/routes/api.js | 75 ++++++ 10 files changed, 869 insertions(+), 211 deletions(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index aa09925..581c8df 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,4 +1,30 @@ const chalk = require('chalk'); +const { doc, getDoc } = require('firebase/firestore'); + +// In-memory cache to prevent excessive DB reads +const globalCache = { + maintenance: { data: null, lastFetch: 0 }, + blacklist: { data: null, lastFetch: 0 }, + guildSettings: new Map() // guildId -> { data, lastFetch } +}; + +const CACHE_TTL = 60 * 1000; // 1 minute + +async function getCachedDoc(db, collection, id, cacheObj) { + const now = Date.now(); + if (cacheObj.data && (now - cacheObj.lastFetch < CACHE_TTL)) { + return cacheObj.data; + } + try { + const snap = await getDoc(doc(db, collection, id)); + cacheObj.data = snap.exists() ? snap.data() : null; + cacheObj.lastFetch = now; + return cacheObj.data; + } catch (err) { + console.error(chalk.red(`[CACHE ERROR] Failed to fetch ${collection}/${id}:`), err); + return cacheObj.data; // Return stale data on error + } +} module.exports = { name: 'interactionCreate', @@ -7,7 +33,52 @@ module.exports = { if (!interaction.isChatInputCommand() && !interaction.isAutocomplete() && !interaction.isModalSubmit()) return; + // 1. Global Checks (Maintenance & Blacklist) + const maintenanceData = await getCachedDoc(client.db, 'bot_settings', 'maintenance', globalCache.maintenance); + const blacklistData = await getCachedDoc(client.db, 'bot_settings', 'blacklist', globalCache.blacklist); + + // Blacklist check + if (blacklistData && blacklistData.users?.includes(interaction.user.id)) { + const message = { content: '[ERR] あなたはボットの使用を制限されています。', ephemeral: true }; + return interaction.isAutocomplete() ? null : interaction.reply(message); + } + + // Maintenance mode check (Bypass for Bot Owners) + if (maintenanceData && maintenanceData.enabled) { + // Get bot owners from application + if (!client.application.owner) await client.application.fetch(); + const owners = client.application.owner.members + ? client.application.owner.members.map(m => m.id) + : [client.application.owner.id]; + + if (!owners.includes(interaction.user.id)) { + const reason = maintenanceData.reason || '現在メンテナンス中です。'; + const message = { content: `[INFO] ${reason}`, ephemeral: true }; + return interaction.isAutocomplete() ? null : interaction.reply(message); + } + } + if (interaction.isChatInputCommand()) { + // 2. Guild-specific Command Check + if (interaction.guildId) { + if (!globalCache.guildSettings.has(interaction.guildId)) { + globalCache.guildSettings.set(interaction.guildId, { data: null, lastFetch: 0 }); + } + const guildSettings = await getCachedDoc( + client.db, + 'guild_settings', + interaction.guildId, + globalCache.guildSettings.get(interaction.guildId) + ); + + if (guildSettings && guildSettings.disabledCommands?.includes(interaction.commandName)) { + return interaction.reply({ + content: `[INFO] このコマンド「${interaction.commandName}」はこのサーバーで無効化されています。`, + ephemeral: true + }); + } + } + const command = client.commands.get(interaction.commandName); if (!command) { console.error(chalk.red(`[ERROR] Unknown command: ${interaction.commandName}`)); diff --git a/package-lock.json b/package-lock.json index d5db253..c7b7c94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,6 @@ "resolved": "https://registry.npmjs.org/@discord-player/extractor/-/extractor-7.1.0.tgz", "integrity": "sha512-/ttNFkN0hacSS/KJNcPP8Dvk1W8+QGbdlbtJNIPHO1oBfEMazs6BimokMG5eCVmSLPb2MaWPGKTjhoQzHLlBlw==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "^16.5.4", "isomorphic-unfetch": "^4.0.2", @@ -213,7 +212,6 @@ "integrity": "sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@discordjs/node-pre-gyp": "^0.4.5", "node-addon-api": "^8.1.0" @@ -274,7 +272,6 @@ "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.19.0.tgz", "integrity": "sha512-UyX6rGEXzVyPzb1yvjHtPfTlnLvB5jX/stAMdiytHhfoydX+98hfympdOwsnTktzr+IRvphxTbdErgYDJkEsvw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/ws": "^8.18.1", "discord-api-types": "^0.38.16", @@ -839,7 +836,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.13.tgz", "integrity": "sha512-OZiDAEK/lDB6xy/XzYAyJJkaDqmQ+BCtOEPLqFvxWKUz5JbBmej7IiiRHdtiIOD/twW7O5AxVsfaaGA/V1bNsA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", @@ -897,7 +893,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.43.tgz", "integrity": "sha512-HM96ZyIblXjAC7TzE8wIk2QhHlSvksYkQ4Ukh1GmEenzkucSNUmUX4QvoKrqeWsLEQ8hdcojABeCV8ybVyZmeg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.10.13", "@firebase/component": "0.6.9", @@ -910,8 +905,7 @@ "version": "0.9.2", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth": { "version": "1.7.9", @@ -1377,7 +1371,6 @@ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz", "integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -2934,7 +2927,6 @@ "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.22.1.tgz", "integrity": "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", @@ -2962,7 +2954,6 @@ "resolved": "https://registry.npmjs.org/distube/-/distube-5.0.7.tgz", "integrity": "sha512-EyxXH2q+SGIgdtKgDPaGvQe9Tyce7nMfps6FV1mt6EUDQg1ld1I2NrLsugCUHaelDpG7zG950dFvv6xryRnMuA==", "license": "MIT", - "peer": true, "dependencies": { "tiny-typed-emitter": "^2.1.0", "undici": "^7.7.0" @@ -3176,7 +3167,6 @@ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3446,7 +3436,6 @@ "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", "hasInstallScript": true, "license": "GPL-3.0-or-later", - "peer": true, "dependencies": { "@derhuerst/http-basic": "^8.2.0", "env-paths": "^2.2.0", @@ -4408,6 +4397,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4424,6 +4414,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4440,6 +4431,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4453,6 +4445,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4469,6 +4462,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4485,6 +4479,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4501,6 +4496,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4517,6 +4513,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4533,6 +4530,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4549,6 +4547,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4565,6 +4564,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4581,6 +4581,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4597,6 +4598,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4613,6 +4615,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4629,6 +4632,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4645,6 +4649,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -6167,7 +6172,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6364,7 +6368,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index c7ac166..3cde43d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@google/generative-ai": "^0.2.1", "@napi-rs/canvas": "^0.1.77", "chalk": "^4.1.2", + "chart.js": "^4.4.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "discord-player": "^7.1.0", diff --git a/public/admin.html b/public/admin.html index 04a8750..893d26f 100644 --- a/public/admin.html +++ b/public/admin.html @@ -45,7 +45,16 @@
情報の読み込みに失敗しました。再ログインしてください。
ログインページへ`; + } + }, + + loadPage: async (pageName) => { + try { + navItems.forEach(item => item.classList.remove('active')); + const activeItem = document.querySelector(`[data-page="${pageName}"]`); + if (activeItem) activeItem.classList.add('active'); + + pageContent.innerHTML = ''; + + if (App.renderers[pageName]) { + await App.renderers[pageName](); + } else { + pageContent.innerHTML = 'ページが見つかりません
'; + } + feather.replace(); + } catch (error) { + pageContent.innerHTML = `エラー: ${error.message}
本当にログアウトしますか?
', + [ + { id: 'cancel-logout', text: 'キャンセル', class: 'btn-secondary' }, + { id: 'confirm-logout', text: 'ログアウト', class: 'btn-danger' } + ] + ); + document.getElementById('cancel-logout').onclick = closeModal; + document.getElementById('confirm-logout').onclick = async () => { + try { + await api.post('/api/admin/logout'); + window.location.href = '/admin-login.html'; + } catch (err) { + showMessage('ログアウトに失敗しました', 'error'); + } + }; + }); + + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('is-open'); + }); + + document.getElementById('global-user-search').addEventListener('keypress', async (e) => { + if (e.key === 'Enter') { + const userId = e.target.value.trim(); + if (!userId) return; + try { + showMessage(`ユーザーID ${userId} を検索中...`, 'info'); + const data = await api.get(`/api/admin/user-search?userId=${userId}`); + + createModal('ユーザー検索結果', ` +所属サーバー (${data.guilds.length}):
+| ユーザーID | アクション |
|---|---|
| ${id} | ++ |
| ブラックリストは空です。 | |
ページが見つかりません
'; - } - } catch (error) { - pageContent.innerHTML = `エラー: ${error.message}
本当にログアウトしますか?
', - [ - { id: 'cancel-logout', text: 'キャンセル', class: 'btn-secondary' }, - { id: 'confirm-logout', text: 'ログアウト', class: 'btn-danger' } - ] - ); - document.getElementById('cancel-logout').onclick = closeModal; - document.getElementById('confirm-logout').onclick = async () => { - try { - await api.post('/api/admin/logout'); - window.location.href = '/admin-login.html'; - } catch (err) { - showMessage('ログアウトに失敗しました', 'error'); - } - }; - }); - - menuToggle.addEventListener('click', () => { - sidebar.classList.toggle('is-open'); - }); - - // Load initial page - const hash = window.location.hash.slice(1) || 'servers'; - loadPage(hash); - - } catch (error) { - loader.innerHTML = `情報の読み込みに失敗しました。再ログインしてください。
ログインページへ`; - } - }; + } +}; - init(); + App.init(); }); diff --git a/public/client.js b/public/client.js index 2ef51e2..01b2b27 100644 --- a/public/client.js +++ b/public/client.js @@ -180,43 +180,241 @@ document.addEventListener('DOMContentLoaded', async () => { console.log('isDirty reset to false.'); }; - // --- Page Rendering --- - const renderers = { - dashboard: async () => { + // --- Dashboard Application Module --- + const App = { + state: { + currentPage: 'dashboard', + isInitialLoad: true + }, + + init: async () => { + try { + guildInfo = await api.get('/api/guild-info'); + document.getElementById('server-icon').src = guildInfo.icon || 'https://cdn.discordapp.com/embed/avatars/0.png'; + document.getElementById('server-name').textContent = guildInfo.name; + + loader.style.display = 'none'; + dashboardWrapper.style.display = 'flex'; + + window.addEventListener('hashchange', App.handleRoute, false); + await App.handleRoute(); + + App.bindGlobalEvents(); + } catch (error) { + console.error('App init error:', error); + loader.innerHTML = `ログインページへ`; + } + }, + + handleRoute: async (event) => { + const proceed = async () => { + resetDirtyState(); + if (window.innerWidth <= 768) { + sidebar.classList.remove('is-open'); + } + + const page = window.location.hash.substring(1) || 'dashboard'; + App.state.currentPage = page; + + navItems.forEach(item => item.classList.toggle('active', item.dataset.page === page)); + pageContent.innerHTML = ''; + + if (App.renderers[page]) { + try { + await App.renderers[page](); + } catch (error) { + console.error(`Render error on ${page}:`, error); + pageContent.innerHTML = ``; + } + } else { + pageContent.innerHTML = ``; + } + + feather.replace(); + window.scrollTo(0, 0); + }; + + if (isDirty) { + if (event && event.type === 'hashchange') { + event.preventDefault(); + const oldHash = event.oldURL.split('#')[1] || 'dashboard'; + history.pushState(null, null, `#${oldHash}`); + } + + createModal('未保存の変更があります', + 'ページを移動すると、保存されていない変更は失われます。本当に移動しますか?
', + [ + { id: 'cancel-nav', text: 'キャンセル', class: 'btn-secondary' }, + { id: 'confirm-nav', text: '移動する', class: 'btn-danger' } + ] + ); + document.getElementById('cancel-nav').onclick = closeModal; + document.getElementById('confirm-nav').onclick = () => { + closeModal(); + if (event) { + const newHash = new URL(event.newURL).hash; + window.location.hash = newHash; + } + proceed(); + }; + } else { + proceed(); + } + }, + + bindGlobalEvents: () => { + logoutBtn.addEventListener('click', () => { + createModal('ログアウトの確認', + '本当にログアウトしますか?
', + [ + { id: 'cancel-logout', text: 'キャンセル', class: 'btn-secondary' }, + { id: 'confirm-logout', text: 'ログアウト', class: 'btn-danger' } + ] + ); + document.getElementById('cancel-logout').onclick = closeModal; + document.getElementById('confirm-logout').onclick = async () => { + try { + isDirty = false; + await api.post('/api/logout'); + window.location.href = '/login'; + } catch (err) { + showMessage('ログアウトに失敗しました', 'error'); + } + }; + }); + + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('is-open'); + }); + + window.addEventListener('beforeunload', (e) => { + if (isDirty) { + e.preventDefault(); + e.returnValue = ''; + return ''; + } + }); + }, + + renderers: { + dashboard: async () => { pageTitle.textContent = 'ダッシュボード'; - const settings = await api.get('/api/settings/guilds'); + const [settings, trends] = await Promise.all([ + api.get('/api/settings/guilds'), + api.get('/api/analytics/trends') + ]); + + const recentLogs = await api.get('/api/audit-logs?limit=5'); + pageContent.innerHTML = `| 日時 | +イベント | +ユーザー | +
|---|---|---|
| ${new Date(log.timestamp.seconds * 1000).toLocaleString()} | +${log.eventType} | +${log.executorTag || 'System'} | +
無効化したコマンドはサーバー内で使用できなくなります。
+ページを移動すると、保存されていない変更は失われます。本当に移動しますか?
', - [ - { id: 'cancel-nav', text: 'キャンセル', class: 'btn-secondary' }, - { id: 'confirm-nav', text: '移動する', class: 'btn-danger' } - ] - ); - document.getElementById('cancel-nav').onclick = closeModal; - document.getElementById('confirm-nav').onclick = () => { - closeModal(); - if (event) { - const newHash = new URL(event.newURL).hash; - window.location.hash = newHash; - } - proceed(); - }; - } else { - proceed(); - } - }; // --- Member Actions --- const addMemberActionListeners = () => { @@ -1601,55 +1861,5 @@ document.addEventListener('DOMContentLoaded', async () => { }; - // --- Initialization --- - const init = async () => { - try { - guildInfo = await api.get('/api/guild-info'); - document.getElementById('server-icon').src = guildInfo.icon || 'https://cdn.discordapp.com/embed/avatars/0.png'; - document.getElementById('server-name').textContent = guildInfo.name; - - loader.style.display = 'none'; - dashboardWrapper.style.display = 'flex'; - - window.addEventListener('hashchange', navigate, false); - await navigate(); - - logoutBtn.addEventListener('click', () => { - createModal('ログアウトの確認', - '本当にログアウトしますか?
', - [ - { id: 'cancel-logout', text: 'キャンセル', class: 'btn-secondary' }, - { id: 'confirm-logout', text: 'ログアウト', class: 'btn-danger' } - ] - ); - document.getElementById('cancel-logout').onclick = closeModal; - document.getElementById('confirm-logout').onclick = async () => { - try { - isDirty = false; - await api.post('/api/logout'); - window.location.href = '/login'; - } catch (err) { - showMessage('ログアウトに失敗しました', 'error'); - } - }; - }); - - menuToggle.addEventListener('click', () => { - sidebar.classList.toggle('is-open'); - }); - - window.addEventListener('beforeunload', (e) => { - if (isDirty) { - e.preventDefault(); - e.returnValue = ''; - return ''; - } - }); - - } catch (error) { - loader.innerHTML = `ログインページへ`; - } - }; - - init(); + App.init(); }); \ No newline at end of file diff --git a/public/dashboard.html b/public/dashboard.html index b48f91e..342019a 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -42,6 +42,8 @@所属サーバー (${data.guilds.length}):
${guild.id}
- ${escapeHTML(guild.id)}
サーバーが見つかりません
'; + + document.getElementById('page-info').textContent = `Page ${data.currentPage} of ${data.totalPages}`; + document.getElementById('prev-page').disabled = data.currentPage <= 1; + document.getElementById('next-page').disabled = data.currentPage >= data.totalPages; + feather.replace(); + } catch (e) { + listEl.innerHTML = '読み込み失敗
'; + } + }; - // 検索機能 - document.getElementById('server-search').addEventListener('input', (e) => { - const searchTerm = e.target.value.toLowerCase(); - document.querySelectorAll('.server-card').forEach(card => { - const serverName = card.dataset.serverName; - card.style.display = serverName.includes(searchTerm) ? 'block' : 'none'; - }); + document.getElementById('server-search').addEventListener('input', () => { + currentPage = 1; + renderServers(); }); + document.getElementById('prev-page').onclick = () => { currentPage--; renderServers(); }; + document.getElementById('next-page').onclick = () => { currentPage++; renderServers(); }; - feather.replace(); + await renderServers(); }, announcements: async () => { @@ -559,15 +596,19 @@ document.addEventListener('DOMContentLoaded', async () => { `; // ログ更新機能 - let logCount = 4; document.getElementById('refresh-logs').addEventListener('click', () => { const logViewer = document.getElementById('log-viewer'); const newLog = document.createElement('p'); newLog.style.color = 'var(--success-color)'; newLog.textContent = `[${new Date().toLocaleTimeString()}] [INFO] ログ更新 - システム正常`; logViewer.appendChild(newLog); + + // Limit logs to prevent infinite growth + while (logViewer.children.length > 50) { + logViewer.removeChild(logViewer.firstChild); + } + logViewer.scrollTop = logViewer.scrollHeight; - logCount++; }); document.getElementById('clear-logs').addEventListener('click', () => { @@ -673,46 +714,104 @@ document.addEventListener('DOMContentLoaded', async () => {| 時刻 | +メモリ (MB) | +Ping (ms) | +
|---|
| 日時 | -イベント | +アクション | ユーザー |
|---|---|---|---|
| ${new Date(log.timestamp.seconds * 1000).toLocaleString()} | -${log.eventType} | -${log.executorTag || 'System'} | +|
| ${new Date(log.timestamp.seconds * 1000).toLocaleString()} | +${escapeHTML(log.eventType)} | +${escapeHTML(log.executorTag || 'System')} | |
| 表示可能なログはありません。 | |||