v1.1.0: Parser versioning, server info, auto-browser, section overviews
- Add parser versioning with Version() method and version display on main screen - Add server model and serial number to Configuration tab and TXT export - Add auto-browser opening on startup with --no-browser flag - Add Restart and Exit buttons with graceful shutdown - Add section overview stats (CPU, Power, Storage, GPU, Network) - Change PCIe Link display to "x16 PCIe Gen4" format - Add Location column to Serials section - Extract BoardInfo from FRU and PlatformId from ThermalConfig Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,48 @@ main {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
/* Parsers info */
|
||||
.parsers-info {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.parsers-title {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.parsers-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.parser-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.parser-name {
|
||||
font-size: 0.85rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.parser-version {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
background: #e8e8e8;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
@@ -375,6 +417,13 @@ footer {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8rem;
|
||||
@@ -403,6 +452,32 @@ footer {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
#restart-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#restart-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
#exit-btn {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#exit-btn:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
@@ -414,6 +489,40 @@ footer {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Server info header */
|
||||
.server-info {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px 8px 0 0;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.server-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.server-info-label {
|
||||
opacity: 0.8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.server-info strong {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.server-info code {
|
||||
background: rgba(255,255,255,0.15);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Configuration tabs */
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
@@ -510,30 +619,46 @@ footer {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Memory overview stats */
|
||||
.memory-overview {
|
||||
/* Section overview stats */
|
||||
.memory-overview,
|
||||
.section-overview {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem 1.5rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border-left: 4px solid #3498db;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.stat-box.model-box {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
border-left-color: #27ae60;
|
||||
}
|
||||
|
||||
.stat-box.model-box .stat-value {
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
initUpload();
|
||||
initTabs();
|
||||
initFilters();
|
||||
loadParsersInfo();
|
||||
});
|
||||
|
||||
// Load and display available parsers
|
||||
async function loadParsersInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/parsers');
|
||||
const data = await response.json();
|
||||
const container = document.getElementById('parsers-info');
|
||||
|
||||
if (data.parsers && data.parsers.length > 0) {
|
||||
let html = '<p class="parsers-title">Поддерживаемые платформы:</p><div class="parsers-list">';
|
||||
data.parsers.forEach(p => {
|
||||
html += `<div class="parser-item">
|
||||
<span class="parser-name">${escapeHtml(p.name)}</span>
|
||||
<span class="parser-version">v${escapeHtml(p.version)}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load parsers info:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload handling
|
||||
function initUpload() {
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
@@ -145,6 +169,14 @@ function renderConfig(data) {
|
||||
|
||||
let html = '';
|
||||
|
||||
// Server info header
|
||||
if (config.board) {
|
||||
html += `<div class="server-info">
|
||||
<div class="server-info-item"><span class="server-info-label">Модель сервера:</span> <strong>${escapeHtml(config.board.product_name || '-')}</strong></div>
|
||||
<div class="server-info-item"><span class="server-info-label">Серийный номер:</span> <code>${escapeHtml(config.board.serial_number || '-')}</code></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Configuration sub-tabs
|
||||
html += `<div class="config-tabs">
|
||||
<button class="config-tab active" data-config-tab="spec">Спецификация</button>
|
||||
@@ -155,7 +187,6 @@ function renderConfig(data) {
|
||||
<button class="config-tab" data-config-tab="gpu">GPU</button>
|
||||
<button class="config-tab" data-config-tab="network">Network</button>
|
||||
<button class="config-tab" data-config-tab="pcie">Device Inventory</button>
|
||||
<button class="config-tab" data-config-tab="fw">Firmware</button>
|
||||
</div>`;
|
||||
|
||||
// Specification tab
|
||||
@@ -174,7 +205,18 @@ function renderConfig(data) {
|
||||
// CPU tab
|
||||
html += '<div class="config-tab-content" id="config-cpu">';
|
||||
if (config.cpus && config.cpus.length > 0) {
|
||||
html += '<h3>Процессоры</h3><table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PPIN</th></tr></thead><tbody>';
|
||||
const cpuCount = config.cpus.length;
|
||||
const cpuModel = config.cpus[0].model || '-';
|
||||
const totalCores = config.cpus.reduce((sum, c) => sum + (c.cores || 0), 0);
|
||||
const totalThreads = config.cpus.reduce((sum, c) => sum + (c.threads || 0), 0);
|
||||
html += `<h3>Процессоры</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${cpuCount}</span><span class="stat-label">Процессоров</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalCores}</span><span class="stat-label">Ядер</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalThreads}</span><span class="stat-label">Потоков</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(cpuModel)}</span><span class="stat-label">Модель</span></div>
|
||||
</div>
|
||||
<table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PPIN</th></tr></thead><tbody>`;
|
||||
config.cpus.forEach(cpu => {
|
||||
html += `<tr>
|
||||
<td>CPU${cpu.socket}</td>
|
||||
@@ -236,7 +278,20 @@ function renderConfig(data) {
|
||||
// Power tab
|
||||
html += '<div class="config-tab-content" id="config-power">';
|
||||
if (config.power_supplies && config.power_supplies.length > 0) {
|
||||
html += '<h3>Блоки питания</h3><table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>';
|
||||
const psuTotal = config.power_supplies.length;
|
||||
const psuPresent = config.power_supplies.filter(p => p.present !== false).length;
|
||||
const psuOK = config.power_supplies.filter(p => p.status === 'OK').length;
|
||||
const psuModel = config.power_supplies[0].model || '-';
|
||||
const psuWattage = config.power_supplies[0].wattage_w || 0;
|
||||
html += `<h3>Блоки питания</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${psuTotal}</span><span class="stat-label">Всего</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${psuPresent}</span><span class="stat-label">Подключено</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${psuOK}</span><span class="stat-label">Работает</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${psuWattage}W</span><span class="stat-label">Мощность</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(psuModel)}</span><span class="stat-label">Модель</span></div>
|
||||
</div>
|
||||
<table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>`;
|
||||
config.power_supplies.forEach(psu => {
|
||||
const statusClass = psu.status === 'OK' ? '' : 'status-warning';
|
||||
html += `<tr>
|
||||
@@ -260,7 +315,22 @@ function renderConfig(data) {
|
||||
// Storage tab
|
||||
html += '<div class="config-tab-content" id="config-storage">';
|
||||
if (config.storage && config.storage.length > 0) {
|
||||
html += '<h3>Накопители</h3><table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>';
|
||||
const storTotal = config.storage.length;
|
||||
const storHDD = config.storage.filter(s => s.type === 'HDD').length;
|
||||
const storSSD = config.storage.filter(s => s.type === 'SSD').length;
|
||||
const storNVMe = config.storage.filter(s => s.type === 'NVMe').length;
|
||||
const totalTB = (config.storage.reduce((sum, s) => sum + (s.size_gb || 0), 0) / 1000).toFixed(1);
|
||||
let typesSummary = [];
|
||||
if (storHDD > 0) typesSummary.push(`${storHDD} HDD`);
|
||||
if (storSSD > 0) typesSummary.push(`${storSSD} SSD`);
|
||||
if (storNVMe > 0) typesSummary.push(`${storNVMe} NVMe`);
|
||||
html += `<h3>Накопители</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${storTotal}</span><span class="stat-label">Всего</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalTB} TB</span><span class="stat-label">Объём</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${typesSummary.join(', ') || '-'}</span><span class="stat-label">По типам</span></div>
|
||||
</div>
|
||||
<table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||
config.storage.forEach(s => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.slot || '-')}</td>
|
||||
@@ -281,7 +351,16 @@ function renderConfig(data) {
|
||||
// GPU tab
|
||||
html += '<div class="config-tab-content" id="config-gpu">';
|
||||
if (config.gpus && config.gpus.length > 0) {
|
||||
html += '<h3>Графические процессоры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>';
|
||||
const gpuCount = config.gpus.length;
|
||||
const gpuModel = config.gpus[0].model || '-';
|
||||
const gpuVendor = config.gpus[0].manufacturer || '-';
|
||||
html += `<h3>Графические процессоры</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${gpuCount}</span><span class="stat-label">Всего GPU</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${escapeHtml(gpuVendor)}</span><span class="stat-label">Производитель</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(gpuModel)}</span><span class="stat-label">Модель</span></div>
|
||||
</div>
|
||||
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||
config.gpus.forEach(gpu => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||||
@@ -301,7 +380,18 @@ function renderConfig(data) {
|
||||
// Network tab
|
||||
html += '<div class="config-tab-content" id="config-network">';
|
||||
if (config.network_adapters && config.network_adapters.length > 0) {
|
||||
html += '<h3>Сетевые адаптеры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>';
|
||||
const nicCount = config.network_adapters.length;
|
||||
const totalPorts = config.network_adapters.reduce((sum, n) => sum + (n.port_count || 0), 0);
|
||||
const nicTypes = [...new Set(config.network_adapters.map(n => n.port_type).filter(t => t))];
|
||||
const nicModels = [...new Set(config.network_adapters.map(n => n.model).filter(m => m))];
|
||||
html += `<h3>Сетевые адаптеры</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${nicCount}</span><span class="stat-label">Адаптеров</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalPorts}</span><span class="stat-label">Портов</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${nicTypes.join(', ') || '-'}</span><span class="stat-label">Тип портов</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(nicModels.join(', ') || '-')}</span><span class="stat-label">Модели</span></div>
|
||||
</div>
|
||||
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>`;
|
||||
config.network_adapters.forEach(nic => {
|
||||
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
||||
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
||||
@@ -326,13 +416,14 @@ function renderConfig(data) {
|
||||
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
||||
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
||||
config.pcie_devices.forEach(p => {
|
||||
const pcieLink = formatPCIeLink(p.link_width, p.link_speed);
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(p.slot || '-')}</td>
|
||||
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||
<td>${escapeHtml(p.device_class || '-')}</td>
|
||||
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
||||
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
||||
<td>x${p.link_width || '-'} ${escapeHtml(p.link_speed || '-')}</td>
|
||||
<td>${pcieLink}</td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
@@ -341,9 +432,6 @@ function renderConfig(data) {
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Firmware tab (content will be populated after firmware loads)
|
||||
html += '<div class="config-tab-content" id="config-fw"><div id="config-fw-content"><p class="no-data">Загрузка...</p></div></div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Initialize config sub-tabs
|
||||
@@ -394,25 +482,6 @@ function renderFirmware(firmware) {
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Render in Config -> Firmware tab
|
||||
const configFwContent = document.getElementById('config-fw-content');
|
||||
if (configFwContent) {
|
||||
if (!firmware || firmware.length === 0) {
|
||||
configFwContent.innerHTML = '<p class="no-data">Нет данных о прошивках</p>';
|
||||
} else {
|
||||
let html = '<h3>Прошивки компонентов</h3><table class="config-table"><thead><tr><th>Компонент</th><th>Модель</th><th>Версия</th></tr></thead><tbody>';
|
||||
firmware.forEach(fw => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(fw.component)}</td>
|
||||
<td>${escapeHtml(fw.model)}</td>
|
||||
<td><code>${escapeHtml(fw.version)}</code></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
configFwContent.innerHTML = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSensors() {
|
||||
@@ -528,9 +597,9 @@ function renderSerials(serials) {
|
||||
row.innerHTML = `
|
||||
<td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
|
||||
<td>${escapeHtml(item.component)}</td>
|
||||
<td>${escapeHtml(item.location || '-')}</td>
|
||||
<td><code>${escapeHtml(item.serial_number)}</code></td>
|
||||
<td>${escapeHtml(item.manufacturer || '-')}</td>
|
||||
<td>${escapeHtml(item.part_number || '-')}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
@@ -606,6 +675,28 @@ async function clearData() {
|
||||
}
|
||||
}
|
||||
|
||||
// Restart app (reload page)
|
||||
function restartApp() {
|
||||
if (confirm('Перезапустить приложение? Все загруженные данные будут потеряны.')) {
|
||||
fetch('/api/clear', { method: 'DELETE' }).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exit app (shutdown server)
|
||||
async function exitApp() {
|
||||
if (confirm('Завершить работу приложения?')) {
|
||||
try {
|
||||
await fetch('/api/shutdown', { method: 'POST' });
|
||||
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
|
||||
} catch (err) {
|
||||
// Server shutdown, connection will fail
|
||||
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return '-';
|
||||
@@ -619,3 +710,24 @@ function escapeHtml(text) {
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatPCIeLink(width, speed) {
|
||||
if (!width && !speed) return '-';
|
||||
|
||||
// Convert GT/s to PCIe generation
|
||||
let gen = '';
|
||||
if (speed) {
|
||||
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
|
||||
if (gtMatch) {
|
||||
const gts = parseFloat(gtMatch[1]);
|
||||
if (gts >= 32) gen = 'Gen5';
|
||||
else if (gts >= 16) gen = 'Gen4';
|
||||
else if (gts >= 8) gen = 'Gen3';
|
||||
else if (gts >= 5) gen = 'Gen2';
|
||||
else if (gts >= 2.5) gen = 'Gen1';
|
||||
}
|
||||
}
|
||||
|
||||
const widthStr = width ? `x${width}` : '';
|
||||
return gen ? `${widthStr} PCIe ${gen}` : `${widthStr} ${speed || ''}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user