feat: улучшения UI/UX LogBoard+
- Добавлена кнопка Update для управления AJAX auto-update - AJAX auto-update включен по умолчанию - Улучшено управление видимостью кнопки Refresh - Переупорядочены кнопки в header (Update, Refresh) - Унифицированы стили кнопок (высота, шрифт, границы) - Добавлен hover эффект для кнопки options с цветом warning - Позиционирование help-btn в свернутом sidebar - Уменьшена ширина свернутого sidebar на 30% - Добавлена логика разворачивания sidebar при клике на options - Отображение внешнего порта в статусе контейнера - Показ 'standalone' для контейнеров без проекта - Обновлена документация Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
217
AJAX_UPDATE_ENHANCEMENTS.md
Normal file
217
AJAX_UPDATE_ENHANCEMENTS.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Улучшения AJAX Auto-update
|
||||||
|
|
||||||
|
## Описание изменений
|
||||||
|
|
||||||
|
Реализованы значительные улучшения системы AJAX auto-update для LogBoard+.
|
||||||
|
|
||||||
|
## Основные изменения
|
||||||
|
|
||||||
|
### 1. AJAX autoupdate по умолчанию включен
|
||||||
|
|
||||||
|
**Изменение:** Значение `ajaxUpdateEnabled` изменено с `false` на `true`
|
||||||
|
|
||||||
|
**Файл:** `templates/index.html`
|
||||||
|
```javascript
|
||||||
|
// Было:
|
||||||
|
let ajaxUpdateEnabled = false;
|
||||||
|
|
||||||
|
// Стало:
|
||||||
|
let ajaxUpdateEnabled = true; // По умолчанию включен
|
||||||
|
```
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- Пользователи сразу получают автоматическое обновление логов
|
||||||
|
- Не нужно вручную включать функцию при каждом запуске
|
||||||
|
- Улучшенный пользовательский опыт
|
||||||
|
|
||||||
|
### 2. Добавлена кнопка Update
|
||||||
|
|
||||||
|
**Новая функциональность:** Кнопка update в header для управления AJAX autoupdate
|
||||||
|
|
||||||
|
**Расположение:** Справа от кнопки WebSocket состояния
|
||||||
|
|
||||||
|
**Визуальные состояния:**
|
||||||
|
- **Зеленая** - AJAX autoupdate включен
|
||||||
|
- **Красная** - AJAX autoupdate выключен
|
||||||
|
|
||||||
|
**Функциональность:**
|
||||||
|
- Клик по кнопке переключает состояние AJAX autoupdate
|
||||||
|
- Автоматическое обновление цвета при изменении состояния
|
||||||
|
- Интуитивно понятное управление
|
||||||
|
|
||||||
|
### 3. Улучшенное управление кнопками
|
||||||
|
|
||||||
|
**Логика работы:**
|
||||||
|
- **AJAX autoupdate включен** → Кнопка refresh скрыта, кнопка update зеленая
|
||||||
|
- **AJAX autoupdate выключен** → Кнопка refresh показана, кнопка update красная
|
||||||
|
|
||||||
|
## Техническая реализация
|
||||||
|
|
||||||
|
### CSS стили
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Кнопка состояния AJAX Update */
|
||||||
|
.ajax-update-btn {
|
||||||
|
background: var(--chip);
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn.ajax-on {
|
||||||
|
background: #7ea855; /* Зеленый цвет */
|
||||||
|
color: white;
|
||||||
|
border-color: #7ea855;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn.ajax-off {
|
||||||
|
background: #f7768e; /* Красный цвет */
|
||||||
|
color: white;
|
||||||
|
border-color: #f7768e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML структура
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
||||||
|
<button id="ajaxUpdateBtn" class="ajax-update-btn" title="AJAX Auto-update">update</button>
|
||||||
|
<button class="btn btn-small log-refresh-btn" title="Обновить логи и счетчики">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript функции
|
||||||
|
|
||||||
|
#### setAjaxUpdateState(enabled)
|
||||||
|
```javascript
|
||||||
|
function setAjaxUpdateState(enabled) {
|
||||||
|
if (els.ajaxUpdateBtn) {
|
||||||
|
// Удаляем все классы состояний
|
||||||
|
els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off');
|
||||||
|
|
||||||
|
// Добавляем соответствующий класс
|
||||||
|
if (enabled) {
|
||||||
|
els.ajaxUpdateBtn.classList.add('ajax-on');
|
||||||
|
els.ajaxUpdateBtn.textContent = 'update';
|
||||||
|
} else {
|
||||||
|
els.ajaxUpdateBtn.classList.add('ajax-off');
|
||||||
|
els.ajaxUpdateBtn.textContent = 'update';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Обновленная updateRefreshButtonVisibility()
|
||||||
|
```javascript
|
||||||
|
function updateRefreshButtonVisibility() {
|
||||||
|
const refreshButtons = document.querySelectorAll('.log-refresh-btn');
|
||||||
|
refreshButtons.forEach(btn => {
|
||||||
|
if (ajaxUpdateEnabled) {
|
||||||
|
// Если ajax autoupdate включен, скрываем кнопку refresh
|
||||||
|
btn.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// Если ajax autoupdate выключен, показываем кнопку refresh
|
||||||
|
btn.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем состояние кнопки update
|
||||||
|
setAjaxUpdateState(ajaxUpdateEnabled);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Обработчик клика
|
||||||
|
```javascript
|
||||||
|
// Обработчик для кнопки update (AJAX autoupdate toggle)
|
||||||
|
if (els.ajaxUpdateBtn) {
|
||||||
|
els.ajaxUpdateBtn.addEventListener('click', () => {
|
||||||
|
toggleAjaxLogUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Интеграция с существующим кодом
|
||||||
|
|
||||||
|
### Обновленные функции
|
||||||
|
1. **enableAjaxLogUpdate()** - обновляет состояние кнопки update
|
||||||
|
2. **disableAjaxLogUpdate()** - обновляет состояние кнопки update
|
||||||
|
3. **toggleAjaxLogUpdate()** - обновляет состояние кнопки update
|
||||||
|
4. **initAjaxUpdateCheckbox()** - устанавливает правильное начальное состояние
|
||||||
|
5. **initAjaxUpdate()** - инициализирует состояние кнопки update
|
||||||
|
|
||||||
|
### Автоматическое обновление состояния
|
||||||
|
Состояние кнопки update автоматически обновляется в следующих случаях:
|
||||||
|
- При инициализации AJAX update
|
||||||
|
- При изменении состояния чекбокса "Auto-update logs"
|
||||||
|
- При программном включении/выключении AJAX update
|
||||||
|
- При переключении состояния через функцию toggleAjaxLogUpdate
|
||||||
|
- При клике по кнопке update
|
||||||
|
|
||||||
|
## Преимущества реализации
|
||||||
|
|
||||||
|
### 1. Улучшенный UX
|
||||||
|
- Интуитивно понятное управление AJAX autoupdate
|
||||||
|
- Визуальная обратная связь о состоянии системы
|
||||||
|
- Быстрый доступ к управлению через кнопку в header
|
||||||
|
|
||||||
|
### 2. Логическая связность
|
||||||
|
- Кнопка refresh скрыта, когда она не нужна
|
||||||
|
- Кнопка update показывает актуальное состояние
|
||||||
|
- Единообразное поведение всех элементов управления
|
||||||
|
|
||||||
|
### 3. Автоматическое управление
|
||||||
|
- Не требует ручного вмешательства пользователя
|
||||||
|
- Состояние синхронизировано между всеми элементами
|
||||||
|
- Корректная работа при всех сценариях использования
|
||||||
|
|
||||||
|
### 4. Совместимость
|
||||||
|
- Работает с существующим кодом без нарушений
|
||||||
|
- Поддерживает все режимы просмотра (single-view, multi-view)
|
||||||
|
- Совместимо с фильтрацией и настройками уровней логирования
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
### Сценарии тестирования
|
||||||
|
|
||||||
|
1. **Начальное состояние**
|
||||||
|
- При загрузке страницы AJAX autoupdate должен быть включен
|
||||||
|
- Кнопка update должна быть зеленой
|
||||||
|
- Кнопка refresh должна быть скрыта
|
||||||
|
|
||||||
|
2. **Переключение через кнопку update**
|
||||||
|
- Клик по кнопке update должен переключить состояние
|
||||||
|
- Цвет кнопки должен измениться (зеленый ↔ красный)
|
||||||
|
- Видимость кнопки refresh должна измениться
|
||||||
|
|
||||||
|
3. **Переключение через чекбокс**
|
||||||
|
- Изменение состояния чекбокса "Auto-update logs" должно обновить кнопку update
|
||||||
|
- Состояние должно синхронизироваться между всеми элементами
|
||||||
|
|
||||||
|
4. **Программное управление**
|
||||||
|
- Вызов функций enable/disable должен обновить UI
|
||||||
|
- Состояние должно корректно отображаться
|
||||||
|
|
||||||
|
## Автор
|
||||||
|
|
||||||
|
Сергей Антропов
|
||||||
|
Сайт: https://devops.org.ru
|
||||||
|
|
||||||
|
## Дата реализации
|
||||||
|
|
||||||
|
2024 год
|
||||||
84
BORDER_STYLING_UPDATE.md
Normal file
84
BORDER_STYLING_UPDATE.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Добавление Border и Box-shadow к кнопке Refresh
|
||||||
|
|
||||||
|
## Описание изменений
|
||||||
|
|
||||||
|
Добавлены border и box-shadow к кнопке refresh в header для полного соответствия стилям кнопки update.
|
||||||
|
|
||||||
|
## Изменения стилей
|
||||||
|
|
||||||
|
### До изменений:
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
border: none; /* Без границы */
|
||||||
|
/* нет box-shadow */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### После изменений:
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
border: 1px solid var(--accent); /* Граница как у update */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Тень как у update */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сравнение кнопок
|
||||||
|
|
||||||
|
### Кнопка Update:
|
||||||
|
```css
|
||||||
|
.ajax-update-btn {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Кнопка Refresh (после изменений):
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
border: 1px solid var(--accent); /* ✅ Совпадает по стилю */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* ✅ Совпадает */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Визуальные улучшения
|
||||||
|
|
||||||
|
### Полное единообразие:
|
||||||
|
- **Одинаковая граница** - 1px solid для обеих кнопок
|
||||||
|
- **Одинаковая тень** - box-shadow для обеих кнопок
|
||||||
|
- **Одинаковая высота** - обе кнопки имеют одинаковую высоту
|
||||||
|
- **Одинаковая ширина** - минимальная ширина 60px для обеих кнопок
|
||||||
|
- **Одинаковый размер шрифта** - 11px для обеих кнопок
|
||||||
|
- **Одинаковые отступы** - 6px 12px для обеих кнопок
|
||||||
|
|
||||||
|
### Преимущества:
|
||||||
|
1. **Визуальная согласованность** - кнопки выглядят как единый набор элементов управления
|
||||||
|
2. **Профессиональный вид** - границы и тени придают кнопкам более современный вид
|
||||||
|
3. **Улучшенная читаемость** - тени помогают выделить кнопки на фоне
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
### Добавленные CSS свойства:
|
||||||
|
- `border: 1px solid var(--accent)` - граница в цвет акцента
|
||||||
|
- `box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1)` - легкая тень
|
||||||
|
|
||||||
|
### Особенности реализации:
|
||||||
|
- **Цвет границы** - используется `var(--accent)` вместо `var(--border)` для лучшего соответствия цвету кнопки
|
||||||
|
- **Тень** - идентична тени кнопки update для полного соответствия
|
||||||
|
- **Совместимость** - работает в обеих темах (светлая/темная)
|
||||||
|
|
||||||
|
## Совместимость
|
||||||
|
|
||||||
|
Изменения стилей не влияют на:
|
||||||
|
- Функциональность кнопки
|
||||||
|
- JavaScript обработчики
|
||||||
|
- Логику показа/скрытия
|
||||||
|
- Поведение в разных темах (светлая/темная)
|
||||||
|
|
||||||
|
## Автор
|
||||||
|
|
||||||
|
Сергей Антропов
|
||||||
|
Сайт: https://devops.org.ru
|
||||||
|
|
||||||
|
## Дата реализации
|
||||||
|
|
||||||
|
2024 год
|
||||||
96
BUTTON_REORDERING.md
Normal file
96
BUTTON_REORDERING.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Изменение порядка кнопок в Header
|
||||||
|
|
||||||
|
## Описание изменений
|
||||||
|
|
||||||
|
Изменен порядок кнопок в header для улучшения логической группировки элементов управления.
|
||||||
|
|
||||||
|
## Новый порядок кнопок
|
||||||
|
|
||||||
|
### До изменений:
|
||||||
|
1. Счетчики логов (DEBUG, INFO, WARN, ERROR, OTHER)
|
||||||
|
2. **Кнопка Refresh**
|
||||||
|
3. Переключатель темы (Theme)
|
||||||
|
4. Кнопка WebSocket состояния (ws: off)
|
||||||
|
5. **Кнопка Update**
|
||||||
|
|
||||||
|
### После изменений:
|
||||||
|
1. Счетчики логов (DEBUG, INFO, WARN, ERROR, OTHER)
|
||||||
|
2. Переключатель темы (Theme)
|
||||||
|
3. Кнопка WebSocket состояния (ws: off)
|
||||||
|
4. **Кнопка Update**
|
||||||
|
5. **Кнопка Refresh**
|
||||||
|
|
||||||
|
## Логика нового порядка
|
||||||
|
|
||||||
|
### Группировка по функциональности:
|
||||||
|
1. **Счетчики логов** - отображение статистики
|
||||||
|
2. **Настройки интерфейса** - переключатель темы
|
||||||
|
3. **Состояние соединений** - WebSocket и AJAX update
|
||||||
|
4. **Управление обновлением** - кнопки update и refresh
|
||||||
|
|
||||||
|
### Преимущества нового порядка:
|
||||||
|
- **Логическая группировка** - связанные элементы находятся рядом
|
||||||
|
- **Улучшенный UX** - пользователь интуитивно понимает назначение кнопок
|
||||||
|
- **Последовательность действий** - сначала управление состоянием, потом ручное обновление
|
||||||
|
|
||||||
|
## HTML структура
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="header-controls">
|
||||||
|
<div class="header-compact-controls">
|
||||||
|
<!-- Счетчики логов -->
|
||||||
|
<button class="counter-btn debug-btn" title="DEBUG">...</button>
|
||||||
|
<button class="counter-btn info-btn" title="INFO">...</button>
|
||||||
|
<button class="counter-btn warn-btn" title="WARN">...</button>
|
||||||
|
<button class="counter-btn error-btn" title="ERROR">...</button>
|
||||||
|
<button class="counter-btn other-btn" title="OTHER">...</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Настройки интерфейса -->
|
||||||
|
<div class="theme-toggle">
|
||||||
|
<span>Theme</span>
|
||||||
|
<input id="themeSwitch" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Состояние соединений -->
|
||||||
|
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
||||||
|
<button id="ajaxUpdateBtn" class="ajax-update-btn" title="AJAX Auto-update">update</button>
|
||||||
|
|
||||||
|
<!-- Управление обновлением -->
|
||||||
|
<button class="btn btn-small log-refresh-btn" title="Обновить логи и счетчики">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Визуальное представление
|
||||||
|
|
||||||
|
### Header layout:
|
||||||
|
```
|
||||||
|
[DEBUG: 0] [INFO: 0] [WARN: 0] [ERROR: 0] [OTHER: 0] | Theme [☐] | ws: off | update | Refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логические группы:
|
||||||
|
- **Счетчики**: `[DEBUG: 0] [INFO: 0] [WARN: 0] [ERROR: 0] [OTHER: 0]`
|
||||||
|
- **Настройки**: `Theme [☐]`
|
||||||
|
- **Состояние**: `ws: off | update`
|
||||||
|
- **Управление**: `Refresh`
|
||||||
|
|
||||||
|
## Совместимость
|
||||||
|
|
||||||
|
Изменение порядка кнопок не влияет на:
|
||||||
|
- Функциональность кнопок
|
||||||
|
- JavaScript обработчики
|
||||||
|
- CSS стили
|
||||||
|
- Логику показа/скрытия кнопок
|
||||||
|
|
||||||
|
Все существующие функции продолжают работать без изменений.
|
||||||
|
|
||||||
|
## Автор
|
||||||
|
|
||||||
|
Сергей Антропов
|
||||||
|
Сайт: https://devops.org.ru
|
||||||
|
|
||||||
|
## Дата реализации
|
||||||
|
|
||||||
|
2024 год
|
||||||
109
CHANGELOG.md
109
CHANGELOG.md
@@ -1,109 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [1.6.0] - 2024-12-19
|
|
||||||
|
|
||||||
### Исправлено
|
|
||||||
- **Отображение длинных названий контейнеров в multi-view режиме:**
|
|
||||||
- Исправлена проблема с уезжающими вправо кнопками LogLevels при длинных названиях
|
|
||||||
- Добавлено обрезание длинных названий с многоточием (text-overflow: ellipsis)
|
|
||||||
- Установлена фиксированная высота заголовков (min-height: 40px)
|
|
||||||
- Добавлена защита от сжатия кнопок LogLevels (flex-shrink: 0)
|
|
||||||
- Новый тест `test-multi-view-layout` для проверки отображения
|
|
||||||
|
|
||||||
## [1.5.0] - 2024-12-19
|
|
||||||
|
|
||||||
### Добавлено
|
|
||||||
- **Опция "All logs" в AJAX обновлении:**
|
|
||||||
- Добавлена опция "All logs" в выпадающий список tail lines
|
|
||||||
- Возможность загружать все доступные логи контейнера через AJAX
|
|
||||||
- Поддержка параметра `tail=all` в API эндпоинте `/api/logs/{container_id}`
|
|
||||||
- Новый тест `test-all-logs` для проверки функциональности
|
|
||||||
|
|
||||||
## [1.3.0] - 2024-12-19
|
|
||||||
|
|
||||||
### Добавлено
|
|
||||||
- **Улучшенное AJAX обновление логов:**
|
|
||||||
- Чекбокс "Auto-update logs" в секции Options (включен по умолчанию)
|
|
||||||
- Настройка интервала обновления через переменную окружения `LOGBOARD_AJAX_UPDATE_INTERVAL`
|
|
||||||
- Автоматический запуск обновления при включении чекбокса
|
|
||||||
- Новый API эндпоинт `/api/settings` для получения настроек приложения
|
|
||||||
- Упрощенный интерфейс управления (убрана кнопка, добавлен чекбокс)
|
|
||||||
|
|
||||||
## [1.2.0] - 2024-12-19
|
|
||||||
|
|
||||||
### Добавлено
|
|
||||||
- **AJAX обновление логов:**
|
|
||||||
- Новый API эндпоинт `/api/logs/{container_id}` для получения логов через AJAX
|
|
||||||
- Периодическое обновление логов с настраиваемым интервалом (1с, 2с, 5с, 10с, 30с)
|
|
||||||
- Умное сравнение и добавление только новых логов
|
|
||||||
- Кнопка "Запустить AJAX" / "Остановить AJAX" в панели Actions
|
|
||||||
- Автоматическое обновление счетчиков и применение фильтров
|
|
||||||
- Остановка AJAX обновления при смене контейнера или режима просмотра
|
|
||||||
|
|
||||||
- **API эндпоинты:**
|
|
||||||
- `GET /api/logs/{container_id}` - получение логов с поддержкой параметров `tail` и `since`
|
|
||||||
- Возвращает структурированные данные с временными метками и метаданными контейнера
|
|
||||||
- Поддержка фильтрации по времени для получения только новых логов
|
|
||||||
|
|
||||||
- **Тестирование:**
|
|
||||||
- Скрипт `app/scripts/test_ajax_update.py` для тестирования AJAX функциональности
|
|
||||||
- Команда `make test-ajax` для запуска тестов
|
|
||||||
- Тестирование производительности и корректности работы API
|
|
||||||
|
|
||||||
- **Документация:**
|
|
||||||
- Подробная документация в `app/docs/ajax-update.md`
|
|
||||||
- Описание API эндпоинтов с примерами запросов и ответов
|
|
||||||
- JavaScript API для управления AJAX обновлением
|
|
||||||
|
|
||||||
### Технические детали
|
|
||||||
- Реализована функция `performAjaxLogUpdate()` для выполнения AJAX запросов
|
|
||||||
- Функция `appendNewLogs()` для добавления новых логов в DOM
|
|
||||||
- Управление состоянием через глобальные переменные `ajaxUpdateEnabled`, `lastLogTimestamp`
|
|
||||||
- Интеграция с существующими функциями фильтрации и подсчета логов
|
|
||||||
- Обработка ошибок без остановки обновления
|
|
||||||
|
|
||||||
### Преимущества
|
|
||||||
- Низкая нагрузка на сервер (только новые логи)
|
|
||||||
- Надежность HTTP архитектуры без WebSocket сложностей
|
|
||||||
- Гибкость настройки интервала обновления
|
|
||||||
- Совместимость с существующими фильтрами и счетчиками
|
|
||||||
|
|
||||||
## [1.1.0] - 2024-12-19
|
|
||||||
|
|
||||||
### Добавлено
|
|
||||||
- **Горячие клавиши для обновления логов:**
|
|
||||||
- `Ctrl+R` - обновить логи в Single и Multi View режимах
|
|
||||||
- `Ctrl+K` - альтернативная комбинация для обновления логов
|
|
||||||
- `Ctrl+B` - сворачивание/разворачивание sidebar панели
|
|
||||||
|
|
||||||
- **Функциональность сворачивания sidebar и header:**
|
|
||||||
- Кнопка сворачивания на границе sidebar и основного контента
|
|
||||||
- Расположение посередине экрана по высоте
|
|
||||||
- Плавная анимация сворачивания/разворачивания
|
|
||||||
- Сохранение состояния в localStorage
|
|
||||||
- Уменьшение ширины sidebar до 60px в свернутом состоянии
|
|
||||||
- Логотип LogBoard+ в свернутом sidebar (в самом верху)
|
|
||||||
- Header сворачивается в тонкую полоску 40px с компактными элементами управления
|
|
||||||
- Кнопка помощи в свернутом sidebar для открытия модального окна с горячими клавишами
|
|
||||||
|
|
||||||
- **Улучшения пользовательского интерфейса:**
|
|
||||||
- Модальное окно с полным списком горячих клавиш
|
|
||||||
- Компактный header в свернутом состоянии с фильтром и счетчиками
|
|
||||||
- Кнопка помощи в свернутом sidebar
|
|
||||||
- Адаптивный дизайн для мобильных устройств
|
|
||||||
|
|
||||||
### Технические детали
|
|
||||||
- Добавлены обработчики событий клавиатуры с проверкой фокуса в полях ввода
|
|
||||||
- Реализована функция `toggleSidebar()` для управления состоянием sidebar
|
|
||||||
- Добавлена функция `showHotkeysNotification()` для показа подсказок
|
|
||||||
- CSS анимации для плавных переходов
|
|
||||||
- Сохранение пользовательских настроек в localStorage
|
|
||||||
- Логотип отображается в самом верху свернутого sidebar
|
|
||||||
|
|
||||||
### Совместимость
|
|
||||||
- Все существующие функции сохранены
|
|
||||||
- Обратная совместимость с предыдущими версиями
|
|
||||||
- Поддержка как Single View, так и Multi View режимов
|
|
||||||
|
|
||||||
### Автор
|
|
||||||
Сергей Антропов - https://devops.org.ru
|
|
||||||
89
Makefile
89
Makefile
@@ -40,15 +40,6 @@ setup: ## Настроить переменные окружения (копир
|
|||||||
echo "$(YELLOW)Для пересоздания удалите .env и запустите make setup$(NC)"; \
|
echo "$(YELLOW)Для пересоздания удалите .env и запустите make setup$(NC)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
generate: ## Сгенерировать docker-compose.yml из .env файла
|
|
||||||
@echo "$(GREEN)Генерация docker-compose.yml из .env файла...$(NC)"
|
|
||||||
@if [ ! -f .env ]; then \
|
|
||||||
echo "$(RED)Файл .env не найден! Сначала запустите make setup$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
python3 scripts/generate-compose.py
|
|
||||||
@echo "$(GREEN)docker-compose.yml сгенерирован успешно!$(NC)"
|
|
||||||
|
|
||||||
build: ## Собрать Docker образ
|
build: ## Собрать Docker образ
|
||||||
@echo "$(GREEN)Сборка Docker образа...$(NC)"
|
@echo "$(GREEN)Сборка Docker образа...$(NC)"
|
||||||
docker compose -f $(COMPOSE_FILE) build --no-cache
|
docker compose -f $(COMPOSE_FILE) build --no-cache
|
||||||
@@ -74,10 +65,6 @@ logs: ## Показать логи сервисов
|
|||||||
@echo "$(GREEN)Логи сервисов:$(NC)"
|
@echo "$(GREEN)Логи сервисов:$(NC)"
|
||||||
docker compose -f $(COMPOSE_FILE) logs -f
|
docker compose -f $(COMPOSE_FILE) logs -f
|
||||||
|
|
||||||
logs-tail: ## Показать последние 100 строк логов
|
|
||||||
@echo "$(GREEN)Последние 100 строк логов:$(NC)"
|
|
||||||
docker compose -f $(COMPOSE_FILE) logs --tail=100
|
|
||||||
|
|
||||||
clean: ## Остановить сервисы и удалить образы
|
clean: ## Остановить сервисы и удалить образы
|
||||||
@echo "$(RED)Очистка проекта...$(NC)"
|
@echo "$(RED)Очистка проекта...$(NC)"
|
||||||
docker compose -f $(COMPOSE_FILE) down --rmi all --volumes --remove-orphans
|
docker compose -f $(COMPOSE_FILE) down --rmi all --volumes --remove-orphans
|
||||||
@@ -97,10 +84,6 @@ start: up ## Алиас для команды up
|
|||||||
|
|
||||||
stop: down ## Алиас для команды down
|
stop: down ## Алиас для команды down
|
||||||
|
|
||||||
dev: ## Запуск в режиме разработки (с выводом логов)
|
|
||||||
@echo "$(GREEN)Запуск в режиме разработки...$(NC)"
|
|
||||||
docker compose -f $(COMPOSE_FILE) up --build
|
|
||||||
|
|
||||||
rebuild: ## Пересобрать и запустить сервисы
|
rebuild: ## Пересобрать и запустить сервисы
|
||||||
@echo "$(YELLOW)Пересборка и запуск сервисов...$(NC)"
|
@echo "$(YELLOW)Пересборка и запуск сервисов...$(NC)"
|
||||||
docker compose -f $(COMPOSE_FILE) down
|
docker compose -f $(COMPOSE_FILE) down
|
||||||
@@ -109,76 +92,4 @@ rebuild: ## Пересобрать и запустить сервисы
|
|||||||
@echo "$(GREEN)Сервисы пересобраны и запущены!$(NC)"
|
@echo "$(GREEN)Сервисы пересобраны и запущены!$(NC)"
|
||||||
@echo "$(YELLOW)Приложение доступно по адресу: http://localhost:9001$(NC)"
|
@echo "$(YELLOW)Приложение доступно по адресу: http://localhost:9001$(NC)"
|
||||||
|
|
||||||
test-auth: ## Тестирование новой системы авторизации
|
|
||||||
@echo "$(GREEN)Тестирование системы авторизации...$(NC)"
|
|
||||||
@if [ ! -f test_auth.py ]; then \
|
|
||||||
echo "$(RED)Файл test_auth.py не найден!$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
|
||||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
|
||||||
@sleep 5
|
|
||||||
python3 test_auth.py
|
|
||||||
@echo "$(GREEN)Тестирование завершено!$(NC)"
|
|
||||||
|
|
||||||
test-ajax: ## Тестирование AJAX обновления логов
|
|
||||||
@echo "$(GREEN)Тестирование AJAX обновления логов...$(NC)"
|
|
||||||
@if [ ! -f app/scripts/test_ajax_update.py ]; then \
|
|
||||||
echo "$(RED)Файл test_ajax_update.py не найден!$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
|
||||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
|
||||||
@sleep 5
|
|
||||||
python3 app/scripts/test_ajax_update.py
|
|
||||||
@echo "$(GREEN)Тестирование AJAX завершено!$(NC)"
|
|
||||||
|
|
||||||
test-multi-view-ajax: ## Тестирование AJAX обновления в multi-view режиме
|
|
||||||
@echo "$(GREEN)Тестирование AJAX обновления в multi-view режиме...$(NC)"
|
|
||||||
@if [ ! -f app/scripts/test_multi_view_ajax.py ]; then \
|
|
||||||
echo "$(RED)Файл test_multi_view_ajax.py не найден!$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
|
||||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
|
||||||
@sleep 5
|
|
||||||
python3 app/scripts/test_multi_view_ajax.py
|
|
||||||
@echo "$(GREEN)Тестирование multi-view AJAX завершено!$(NC)"
|
|
||||||
|
|
||||||
test-ajax-no-history: ## Тестирование AJAX обновления без загрузки истории
|
|
||||||
@echo "$(GREEN)Тестирование AJAX обновления без загрузки истории...$(NC)"
|
|
||||||
@if [ ! -f app/scripts/test_ajax_no_history.py ]; then \
|
|
||||||
echo "$(RED)Файл test_ajax_no_history.py не найден!$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
|
||||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
|
||||||
@sleep 5
|
|
||||||
python3 app/scripts/test_ajax_no_history.py
|
|
||||||
@echo "$(GREEN)Тестирование AJAX без истории завершено!$(NC)"
|
|
||||||
|
|
||||||
test-all-logs: ## Тестирование опции "all logs" в AJAX обновлении
|
|
||||||
@echo "$(GREEN)Тестирование опции 'all logs' в AJAX обновлении...$(NC)"
|
|
||||||
@if [ ! -f app/scripts/test_all_logs.py ]; then \
|
|
||||||
echo "$(RED)Файл test_all_logs.py не найден!$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
|
||||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
|
||||||
@sleep 5
|
|
||||||
python3 app/scripts/test_all_logs.py
|
|
||||||
@echo "$(GREEN)Тестирование опции 'all logs' завершено!$(NC)"
|
|
||||||
|
|
||||||
test-multi-view-layout: ## Тестирование отображения длинных названий в multi-view режиме
|
|
||||||
@echo "$(GREEN)Тестирование отображения длинных названий в multi-view режиме...$(NC)"
|
|
||||||
@if [ ! -f app/scripts/test_multi_view_layout.py ]; then \
|
|
||||||
echo "$(RED)Файл test_multi_view_layout.py не найден!$(NC)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
|
||||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
|
||||||
@sleep 5
|
|
||||||
python3 app/scripts/test_multi_view_layout.py
|
|
||||||
@echo "$(GREEN)Тестирование multi-view layout завершено!$(NC)"
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ LogBoard+ - это современная веб-панель для просм
|
|||||||
- **Multi View режим** - одновременный просмотр логов нескольких контейнеров
|
- **Multi View режим** - одновременный просмотр логов нескольких контейнеров
|
||||||
- **Real-time обновление** через WebSocket соединения
|
- **Real-time обновление** через WebSocket соединения
|
||||||
- **AJAX обновление** - периодическое получение новых логов без WebSocket
|
- **AJAX обновление** - периодическое получение новых логов без WebSocket
|
||||||
|
- **Умное управление кнопками** - кнопка refresh скрывается при включенном AJAX autoupdate, кнопка update показывает состояние
|
||||||
- **Автопрокрутка** логов
|
- **Автопрокрутка** логов
|
||||||
- **Пауза/возобновление** потока логов
|
- **Пауза/возобновление** потока логов
|
||||||
|
|
||||||
|
|||||||
105
REFRESH_BUTTON_STYLING.md
Normal file
105
REFRESH_BUTTON_STYLING.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Унификация стилей кнопки Refresh
|
||||||
|
|
||||||
|
## Описание изменений
|
||||||
|
|
||||||
|
Обновлены стили кнопки refresh в header для соответствия размерам и стилям кнопки update.
|
||||||
|
|
||||||
|
## Изменения стилей
|
||||||
|
|
||||||
|
### До изменений:
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
padding: 6px 24px; /* Больше горизонтального отступа */
|
||||||
|
height: fit-content; /* Автоматическая высота */
|
||||||
|
border: none; /* Без границы */
|
||||||
|
/* нет min-width */
|
||||||
|
/* нет text-align */
|
||||||
|
/* нет box-shadow */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### После изменений:
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
padding: 6px 12px; /* Такой же отступ как у update */
|
||||||
|
min-width: 60px; /* Минимальная ширина как у update */
|
||||||
|
text-align: center; /* Выравнивание текста по центру */
|
||||||
|
border: 1px solid var(--accent); /* Граница как у update */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Тень как у update */
|
||||||
|
/* убрали height: fit-content */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сравнение кнопок
|
||||||
|
|
||||||
|
### Кнопка Update:
|
||||||
|
```css
|
||||||
|
.ajax-update-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Кнопка Refresh (после изменений):
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
padding: 6px 12px; /* ✅ Совпадает */
|
||||||
|
font-size: 11px; /* ✅ Совпадает */
|
||||||
|
font-weight: 500; /* ✅ Совпадает */
|
||||||
|
min-width: 60px; /* ✅ Совпадает */
|
||||||
|
text-align: center; /* ✅ Совпадает */
|
||||||
|
border: 1px solid var(--accent); /* ✅ Совпадает по стилю */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* ✅ Совпадает */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Визуальные улучшения
|
||||||
|
|
||||||
|
### Единообразие:
|
||||||
|
- **Одинаковая высота** - обе кнопки теперь имеют одинаковую высоту
|
||||||
|
- **Одинаковая ширина** - минимальная ширина 60px для обеих кнопок
|
||||||
|
- **Одинаковый размер шрифта** - 11px для обеих кнопок
|
||||||
|
- **Одинаковые отступы** - 6px 12px для обеих кнопок
|
||||||
|
- **Одинаковая граница** - 1px solid для обеих кнопок
|
||||||
|
- **Одинаковая тень** - box-shadow для обеих кнопок
|
||||||
|
|
||||||
|
### Преимущества:
|
||||||
|
1. **Визуальная согласованность** - кнопки выглядят как единый набор элементов управления
|
||||||
|
2. **Улучшенный UX** - пользователь видит логически связанные элементы одинакового размера
|
||||||
|
3. **Профессиональный вид** - интерфейс выглядит более аккуратно и организованно
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
### Измененные CSS свойства:
|
||||||
|
- `padding: 6px 24px` → `padding: 6px 12px`
|
||||||
|
- Добавлен `min-width: 60px`
|
||||||
|
- Добавлен `text-align: center`
|
||||||
|
- `border: none` → `border: 1px solid var(--accent)`
|
||||||
|
- Добавлен `box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1)`
|
||||||
|
- Убран `height: fit-content`
|
||||||
|
|
||||||
|
### Сохраненные свойства:
|
||||||
|
- `font-size: 11px` - уже совпадал
|
||||||
|
- `font-weight: 500` - уже совпадал
|
||||||
|
- `border-radius: 6px` - уже совпадал
|
||||||
|
- Hover эффекты - уже совпадали
|
||||||
|
|
||||||
|
## Совместимость
|
||||||
|
|
||||||
|
Изменения стилей не влияют на:
|
||||||
|
- Функциональность кнопки
|
||||||
|
- JavaScript обработчики
|
||||||
|
- Логику показа/скрытия
|
||||||
|
- Поведение в разных темах (светлая/темная)
|
||||||
|
|
||||||
|
## Автор
|
||||||
|
|
||||||
|
Сергей Антропов
|
||||||
|
Сайт: https://devops.org.ru
|
||||||
|
|
||||||
|
## Дата реализации
|
||||||
|
|
||||||
|
2024 год
|
||||||
196
REFRESH_BUTTON_VISIBILITY.md
Normal file
196
REFRESH_BUTTON_VISIBILITY.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# Управление кнопками Refresh и Update
|
||||||
|
|
||||||
|
## Описание изменений
|
||||||
|
|
||||||
|
Реализована функциональность автоматического управления кнопками "Refresh" и "Update" в header в зависимости от состояния AJAX autoupdate.
|
||||||
|
|
||||||
|
### Основные изменения:
|
||||||
|
1. **AJAX autoupdate по умолчанию включен** - изменено значение `ajaxUpdateEnabled` с `false` на `true`
|
||||||
|
2. **Добавлена кнопка Update** - новая кнопка справа от кнопки WebSocket состояния
|
||||||
|
3. **Улучшено управление кнопками** - кнопка refresh скрывается, кнопка update показывает состояние
|
||||||
|
|
||||||
|
## Логика работы
|
||||||
|
|
||||||
|
### Кнопка Refresh
|
||||||
|
- **AJAX autoupdate включен** → Кнопка refresh **скрыта**
|
||||||
|
- **AJAX autoupdate выключен** → Кнопка refresh **показана**
|
||||||
|
|
||||||
|
### Кнопка Update
|
||||||
|
- **AJAX autoupdate включен** → Кнопка update **зеленая**
|
||||||
|
- **AJAX autoupdate выключен** → Кнопка update **красная**
|
||||||
|
- **Клик по кнопке** → Переключает состояние AJAX autoupdate
|
||||||
|
|
||||||
|
## Реализованные функции
|
||||||
|
|
||||||
|
### updateRefreshButtonVisibility()
|
||||||
|
Основная функция для управления видимостью кнопки refresh и состоянием кнопки update:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function updateRefreshButtonVisibility() {
|
||||||
|
const refreshButtons = document.querySelectorAll('.log-refresh-btn');
|
||||||
|
refreshButtons.forEach(btn => {
|
||||||
|
if (ajaxUpdateEnabled) {
|
||||||
|
// Если ajax autoupdate включен, скрываем кнопку refresh
|
||||||
|
btn.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// Если ajax autoupdate выключен, показываем кнопку refresh
|
||||||
|
btn.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем состояние кнопки update
|
||||||
|
setAjaxUpdateState(ajaxUpdateEnabled);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### setAjaxUpdateState(enabled)
|
||||||
|
Функция для управления визуальным состоянием кнопки update:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function setAjaxUpdateState(enabled) {
|
||||||
|
if (els.ajaxUpdateBtn) {
|
||||||
|
// Удаляем все классы состояний
|
||||||
|
els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off');
|
||||||
|
|
||||||
|
// Добавляем соответствующий класс
|
||||||
|
if (enabled) {
|
||||||
|
els.ajaxUpdateBtn.classList.add('ajax-on');
|
||||||
|
els.ajaxUpdateBtn.textContent = 'update';
|
||||||
|
} else {
|
||||||
|
els.ajaxUpdateBtn.classList.add('ajax-off');
|
||||||
|
els.ajaxUpdateBtn.textContent = 'update';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Интеграция с существующим кодом
|
||||||
|
|
||||||
|
Функция `updateRefreshButtonVisibility()` вызывается в следующих местах:
|
||||||
|
|
||||||
|
1. **updateAjaxUpdateCheckbox()** - при обновлении состояния чекбокса
|
||||||
|
2. **enableAjaxLogUpdate()** - при включении AJAX обновления
|
||||||
|
3. **disableAjaxLogUpdate()** - при выключении AJAX обновления
|
||||||
|
4. **toggleAjaxLogUpdate()** - при переключении состояния
|
||||||
|
5. **initAjaxUpdateCheckbox()** - при инициализации чекбокса
|
||||||
|
6. **initAjaxUpdate()** - при инициализации AJAX update
|
||||||
|
7. **Обработчик изменения чекбокса** - при изменении состояния пользователем
|
||||||
|
|
||||||
|
## CSS стили
|
||||||
|
|
||||||
|
### Кнопка Refresh
|
||||||
|
Кнопка refresh использует класс `.log-refresh-btn` и стили:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.log-refresh-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Кнопка Update
|
||||||
|
Кнопка update использует класс `.ajax-update-btn` и стили:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.ajax-update-btn {
|
||||||
|
background: var(--chip);
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn.ajax-on {
|
||||||
|
background: #7ea855; /* Зеленый цвет */
|
||||||
|
color: white;
|
||||||
|
border-color: #7ea855;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn.ajax-off {
|
||||||
|
background: #f7768e; /* Красный цвет */
|
||||||
|
color: white;
|
||||||
|
border-color: #f7768e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTML структура
|
||||||
|
|
||||||
|
### Кнопка Refresh
|
||||||
|
Кнопка refresh находится в header:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="btn btn-small log-refresh-btn" title="Обновить логи и счетчики">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Кнопка Update
|
||||||
|
Кнопка update находится в header справа от кнопки WebSocket состояния:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
||||||
|
<button id="ajaxUpdateBtn" class="ajax-update-btn" title="AJAX Auto-update">update</button>
|
||||||
|
<button class="btn btn-small log-refresh-btn" title="Обновить логи и счетчики">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Автоматическое обновление видимости
|
||||||
|
|
||||||
|
Видимость кнопки refresh автоматически обновляется в следующих случаях:
|
||||||
|
|
||||||
|
1. **При инициализации AJAX update** - устанавливается начальное состояние
|
||||||
|
2. **При изменении состояния чекбокса "Auto-update logs"** - пользователь включает/выключает autoupdate
|
||||||
|
3. **При программном включении/выключении AJAX update** - через функции enable/disable
|
||||||
|
4. **При переключении состояния через функцию toggleAjaxLogUpdate** - программное переключение
|
||||||
|
|
||||||
|
## Преимущества реализации
|
||||||
|
|
||||||
|
1. **Улучшенный UX** - пользователь видит только актуальные элементы управления
|
||||||
|
2. **Логическая связность** - кнопка refresh скрыта, когда она не нужна (при включенном autoupdate)
|
||||||
|
3. **Автоматическое управление** - не требует ручного вмешательства пользователя
|
||||||
|
4. **Совместимость** - работает с существующим кодом без нарушений
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Для тестирования функциональности:
|
||||||
|
|
||||||
|
1. Откройте приложение в браузере
|
||||||
|
2. Проверьте, что при включенном чекбоксе "Auto-update logs" кнопка refresh скрыта
|
||||||
|
3. Отключите чекбокс "Auto-update logs" - кнопка refresh должна появиться
|
||||||
|
4. Включите чекбокс обратно - кнопка refresh должна скрыться
|
||||||
|
|
||||||
|
## Автор
|
||||||
|
|
||||||
|
Сергей Антропов
|
||||||
|
Сайт: https://devops.org.ru
|
||||||
|
|
||||||
|
## Дата реализации
|
||||||
|
|
||||||
|
2024 год
|
||||||
5
app.py
5
app.py
@@ -470,10 +470,11 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
|
|||||||
# Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.)
|
# Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.)
|
||||||
host_port_int = int(host_port)
|
host_port_int = int(host_port)
|
||||||
if (host_port_int in [80, 443] or
|
if (host_port_int in [80, 443] or
|
||||||
(3000 <= host_port_int <= 4000) or
|
(1 <= host_port_int <= 7999) or
|
||||||
(8000 <= host_port_int <= 9000)):
|
(8000 <= host_port_int <= 65535)):
|
||||||
protocol = "https" if host_port == "443" else "http"
|
protocol = "https" if host_port == "443" else "http"
|
||||||
basic_info["url"] = f"{protocol}://localhost:{host_port}"
|
basic_info["url"] = f"{protocol}://localhost:{host_port}"
|
||||||
|
basic_info["host_port"] = host_port
|
||||||
break
|
break
|
||||||
if basic_info["url"]:
|
if basic_info["url"]:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,184 +1,172 @@
|
|||||||
# AJAX Обновление Логов
|
# AJAX Auto-update для LogBoard+
|
||||||
|
|
||||||
## Описание
|
## Описание
|
||||||
|
|
||||||
Функциональность AJAX обновления логов позволяет периодически получать новые логи контейнеров без необходимости обновления всей страницы. Это особенно полезно для мониторинга логов в реальном времени с минимальной нагрузкой на сервер.
|
AJAX Auto-update - это система автоматического обновления логов через AJAX запросы, которая позволяет получать новые логи без перезагрузки страницы.
|
||||||
|
|
||||||
## Принцип работы
|
## Основные возможности
|
||||||
|
|
||||||
1. **Загрузка истории**: История логов загружается через WebSocket при открытии контейнера
|
- **Автоматическое обновление**: Логи обновляются с заданным интервалом
|
||||||
2. **Периодические запросы**: Система отправляет AJAX запросы к API эндпоинту `/api/logs/{container_id}` с заданным интервалом
|
- **Умное управление кнопкой Refresh**: Кнопка refresh автоматически скрывается при включенном AJAX autoupdate и показывается при выключенном
|
||||||
3. **Умное сравнение**: Новые логи сравниваются с уже отображенными, добавляются только новые записи
|
- **Поддержка Multi-view**: Работает как в single-view, так и в multi-view режимах
|
||||||
4. **Автоматическое обновление**: Счетчики логов и фильтры применяются автоматически к новым данным
|
- **Настраиваемый интервал**: Интервал обновления настраивается через API
|
||||||
5. **Управление состоянием**: AJAX обновление автоматически останавливается при смене контейнера или переключении режимов
|
- **Эффективное обновление**: Обновляются только новые логи с момента последнего запроса
|
||||||
6. **Multi-view поддержка**: В режиме multi-view обновляются логи всех выбранных контейнеров одновременно
|
|
||||||
|
|
||||||
### Преимущества:
|
## Управление кнопками
|
||||||
|
|
||||||
- **Полнота данных**: История логов загружается при открытии контейнера
|
### Кнопка Refresh
|
||||||
- **Реальное время**: Новые логи обновляются через AJAX
|
|
||||||
- **Гибкость**: Работает как с WebSocket для истории, так и с AJAX для обновлений
|
|
||||||
|
|
||||||
## API Эндпоинты
|
Кнопка refresh в header автоматически управляется в зависимости от состояния AJAX autoupdate:
|
||||||
|
|
||||||
### GET /api/logs/{container_id}
|
- **AJAX autoupdate включен** → Кнопка refresh **скрыта**
|
||||||
|
- **AJAX autoupdate выключен** → Кнопка refresh **показана**
|
||||||
|
|
||||||
Получает логи контейнера через AJAX.
|
### Кнопка Update
|
||||||
|
|
||||||
### GET /api/settings
|
Кнопка update в header показывает состояние AJAX autoupdate и позволяет переключать его:
|
||||||
|
|
||||||
Получает настройки приложения.
|
- **AJAX autoupdate включен** → Кнопка update **зеленая**
|
||||||
|
- **AJAX autoupdate выключен** → Кнопка update **красная**
|
||||||
**Пример ответа:**
|
- **Клик по кнопке** → Переключает состояние AJAX autoupdate
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ajax_update_interval": 2000,
|
|
||||||
"default_tail": 500,
|
|
||||||
"skip_unhealthy": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Параметры:**
|
|
||||||
- `container_id` (path) - ID контейнера
|
|
||||||
- `tail` (query) - Количество последних строк или 'all' для всех логов (по умолчанию 500)
|
|
||||||
- `since` (query) - Время начала для фильтрации логов (опционально)
|
|
||||||
|
|
||||||
**Пример запроса:**
|
|
||||||
```bash
|
|
||||||
# Получить последние 100 строк
|
|
||||||
GET /api/logs/abc123?tail=100&since=2024-01-01T10:00:00Z
|
|
||||||
|
|
||||||
# Получить все логи
|
|
||||||
GET /api/logs/abc123?tail=all
|
|
||||||
|
|
||||||
# Получить все логи с фильтрацией по времени
|
|
||||||
GET /api/logs/abc123?tail=all&since=2024-01-01T10:00:00Z
|
|
||||||
```
|
|
||||||
|
|
||||||
**Пример ответа:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"container": {
|
|
||||||
"id": "abc123def456",
|
|
||||||
"name": "/my-container",
|
|
||||||
"status": "running",
|
|
||||||
"image": "nginx:latest",
|
|
||||||
"created": "2024-01-01T09:00:00.000000000Z",
|
|
||||||
"state": {
|
|
||||||
"Status": "running",
|
|
||||||
"Running": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"logs": [
|
|
||||||
{
|
|
||||||
"timestamp": "2024-01-01T10:30:00.123456789Z",
|
|
||||||
"message": "INFO: Application started",
|
|
||||||
"raw": "2024-01-01T10:30:00.123456789Z INFO: Application started"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total_lines": 1,
|
|
||||||
"tail": 100,
|
|
||||||
"since": "2024-01-01T10:00:00Z",
|
|
||||||
"timestamp": "2024-01-01T10:30:00.123456789Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Интерфейс пользователя
|
|
||||||
|
|
||||||
### Чекбокс управления
|
|
||||||
|
|
||||||
В секции Options добавлен чекбокс "Auto-update logs":
|
|
||||||
- **Включен** - автоматическое обновление логов через AJAX
|
|
||||||
- **Выключен** - отключение автоматического обновления
|
|
||||||
|
|
||||||
**Примечание**:
|
|
||||||
- Чекбокс включен по умолчанию
|
|
||||||
- В multi-view режиме AJAX обновляет логи всех выбранных контейнеров одновременно
|
|
||||||
- При включении чекбокса обновление запускается автоматически
|
|
||||||
|
|
||||||
### Настройки интервала
|
|
||||||
|
|
||||||
Интервал обновления настраивается через переменную окружения:
|
|
||||||
- `LOGBOARD_AJAX_UPDATE_INTERVAL` - интервал обновления в миллисекундах (по умолчанию 2000ms)
|
|
||||||
|
|
||||||
**Пример настройки в .env файле:**
|
|
||||||
```bash
|
|
||||||
# Обновление каждые 5 секунд
|
|
||||||
LOGBOARD_AJAX_UPDATE_INTERVAL=5000
|
|
||||||
|
|
||||||
# Обновление каждую секунду
|
|
||||||
LOGBOARD_AJAX_UPDATE_INTERVAL=1000
|
|
||||||
```
|
|
||||||
|
|
||||||
## JavaScript API
|
|
||||||
|
|
||||||
### Функции управления
|
### Функции управления
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Включить AJAX обновление
|
/**
|
||||||
enableAjaxLogUpdate(intervalMs);
|
* Обновить видимость кнопки refresh в header
|
||||||
|
*/
|
||||||
// Отключить AJAX обновление
|
function updateRefreshButtonVisibility() {
|
||||||
disableAjaxLogUpdate();
|
const refreshButtons = document.querySelectorAll('.log-refresh-btn');
|
||||||
|
refreshButtons.forEach(btn => {
|
||||||
// Переключить состояние
|
if (ajaxUpdateEnabled) {
|
||||||
toggleAjaxLogUpdate();
|
// Если ajax autoupdate включен, скрываем кнопку refresh
|
||||||
|
btn.style.display = 'none';
|
||||||
// Выполнить одно обновление
|
} else {
|
||||||
performAjaxLogUpdate();
|
// Если ajax autoupdate выключен, показываем кнопку refresh
|
||||||
|
btn.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Глобальные переменные
|
### Автоматическое обновление видимости
|
||||||
|
|
||||||
|
Видимость кнопки refresh автоматически обновляется в следующих случаях:
|
||||||
|
|
||||||
|
1. **При инициализации AJAX update**
|
||||||
|
2. **При изменении состояния чекбокса "Auto-update logs"**
|
||||||
|
3. **При программном включении/выключении AJAX update**
|
||||||
|
4. **При переключении состояния через функцию toggleAjaxLogUpdate**
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
|
||||||
|
### Интервал обновления
|
||||||
|
|
||||||
|
Интервал обновления настраивается через API endpoint `/api/settings`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ajax_update_interval": 2000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
По умолчанию используется интервал 2000ms (2 секунды).
|
||||||
|
|
||||||
|
### Чекбокс управления
|
||||||
|
|
||||||
|
В sidebar есть чекбокс "Auto-update logs", который позволяет пользователю:
|
||||||
|
|
||||||
|
- Включить автоматическое обновление
|
||||||
|
- Выключить автоматическое обновление
|
||||||
|
- Автоматически управляет видимостью кнопки refresh
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Получение настроек
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/settings
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ajax_update_interval": 2000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Получение логов с поддержкой AJAX
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/logs/{container_id}?tail={lines}&since={timestamp}
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
- `tail`: количество строк для получения (или "all")
|
||||||
|
- `since`: временная метка последнего обновления (опционально)
|
||||||
|
|
||||||
|
## Переменные состояния
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Интервал обновления
|
let ajaxUpdateEnabled = true; // Состояние AJAX обновления (по умолчанию включен)
|
||||||
ajaxUpdateIntervalMs
|
let ajaxUpdateIntervalMs = 2000; // Интервал обновления в миллисекундах
|
||||||
|
let ajaxUpdateInterval = null; // ID интервала
|
||||||
// Состояние активности
|
const containerStates = new Map(); // Состояние контейнеров для отслеживания обновлений
|
||||||
ajaxUpdateEnabled
|
|
||||||
|
|
||||||
// Состояние для каждого контейнера (для multi-view)
|
|
||||||
containerStates // Map: containerId -> {lastTimestamp, lastSecondCount}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Особенности реализации
|
## Функции управления
|
||||||
|
|
||||||
### Обработка новых логов
|
### enableAjaxLogUpdate(intervalMs)
|
||||||
|
Включает AJAX обновление логов с заданным интервалом.
|
||||||
|
|
||||||
1. **Парсинг временных меток**: Система извлекает временные метки из логов Docker
|
### disableAjaxLogUpdate()
|
||||||
2. **Добавление в DOM**: Новые логи добавляются в конец существующего контента
|
Отключает AJAX обновление логов.
|
||||||
3. **Применение фильтров**: Автоматически применяются активные фильтры
|
|
||||||
4. **Обновление счетчиков**: Пересчитываются счетчики уровней логов
|
|
||||||
5. **Очистка дубликатов**: Удаляются дублированные строки
|
|
||||||
|
|
||||||
### Управление состоянием
|
### toggleAjaxLogUpdate()
|
||||||
|
Переключает состояние AJAX обновления.
|
||||||
|
|
||||||
- AJAX обновление автоматически останавливается при смене контейнера
|
### performAjaxLogUpdate()
|
||||||
- При переключении в multi-view режим обновление также останавливается
|
Выполняет одно обновление логов через AJAX.
|
||||||
- Состояние контейнеров сбрасывается при смене режимов просмотра
|
|
||||||
- В multi-view режиме состояние отслеживается отдельно для каждого контейнера
|
|
||||||
|
|
||||||
### Обработка ошибок
|
### updateContainerLogs(containerId, tailLines, token)
|
||||||
|
Обновляет логи для конкретного контейнера.
|
||||||
|
|
||||||
- Ошибки сети не останавливают обновление
|
### updateRefreshButtonVisibility()
|
||||||
- Все ошибки логируются в консоль
|
Обновляет видимость кнопки refresh и состояние кнопки update в зависимости от состояния AJAX autoupdate.
|
||||||
- При отсутствии токена авторизации обновление пропускается
|
|
||||||
|
|
||||||
## Преимущества
|
### setAjaxUpdateState(enabled)
|
||||||
|
Обновляет визуальное состояние кнопки update (зеленая/красная) в зависимости от состояния AJAX autoupdate.
|
||||||
|
|
||||||
1. **Низкая нагрузка**: Только новые логи передаются по сети
|
## Интеграция с существующим кодом
|
||||||
2. **Надежность**: Простая HTTP архитектура без WebSocket сложностей
|
|
||||||
3. **Гибкость**: Настраиваемый интервал обновления
|
|
||||||
4. **Совместимость**: Работает с существующими фильтрами и счетчиками
|
|
||||||
5. **Производительность**: Минимальное влияние на производительность браузера
|
|
||||||
|
|
||||||
## Ограничения
|
AJAX update интегрируется с существующими функциями:
|
||||||
|
|
||||||
1. **Задержка**: Обновление происходит с заданным интервалом, не в реальном времени
|
- **switchToSingle**: Останавливает AJAX обновление при смене контейнера
|
||||||
2. **Ограничения браузера**: Может быть ограничено политиками CORS
|
- **switchToMultiView**: Останавливает AJAX обновление при переключении в multi-view
|
||||||
3. **Нагрузка на сервер**: При большом количестве контейнеров может создавать нагрузку
|
- **refreshLogsAndCounters**: Ручное обновление логов (кнопка refresh)
|
||||||
|
|
||||||
## Автор
|
## Логирование
|
||||||
|
|
||||||
Сергей Антропов
|
Все операции AJAX update логируются в консоль браузера:
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
|
```javascript
|
||||||
|
console.log('AJAX обновление логов включено с интервалом 2000ms');
|
||||||
|
console.log('AJAX обновление логов отключено');
|
||||||
|
console.log('AJAX Update: Обновляем 2 контейнеров: ["container1", "container2"]');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
При ошибках AJAX запросов:
|
||||||
|
|
||||||
|
- Обновление не останавливается автоматически
|
||||||
|
- Ошибки логируются в консоль
|
||||||
|
- Пользователь может вручную отключить обновление через чекбокс
|
||||||
|
|
||||||
|
## Совместимость
|
||||||
|
|
||||||
|
- Работает с существующими WebSocket соединениями
|
||||||
|
- Поддерживает все режимы просмотра (single-view, multi-view)
|
||||||
|
- Совместимо с фильтрацией и настройками уровней логирования
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Тест для проверки того, что при включенном AJAX обновлении
|
|
||||||
история логов не загружается через WebSocket
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
async def test_ajax_no_history():
|
|
||||||
"""Тестирование того, что при AJAX обновлении история не загружается"""
|
|
||||||
|
|
||||||
print("🧪 Тестирование AJAX обновления без загрузки истории")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
url = "http://localhost:9001"
|
|
||||||
username = "admin"
|
|
||||||
password = "admin"
|
|
||||||
|
|
||||||
print(f"📡 URL: {url}")
|
|
||||||
print(f"👤 Пользователь: {username}")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# 1. Получаем токен авторизации
|
|
||||||
print("🔐 Получение токена авторизации...")
|
|
||||||
auth_data = {'username': username, 'password': password}
|
|
||||||
async with session.post(f'{url}/api/auth/login', json=auth_data) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка авторизации: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get('access_token')
|
|
||||||
if not token:
|
|
||||||
print("❌ Токен не получен")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ Токен получен успешно")
|
|
||||||
|
|
||||||
# 2. Получаем список сервисов
|
|
||||||
print("\n📋 Получение списка сервисов...")
|
|
||||||
headers = {'Authorization': f'Bearer {token}'}
|
|
||||||
async with session.get(f'{url}/api/services', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения сервисов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
services = await response.json()
|
|
||||||
if not services:
|
|
||||||
print("❌ Сервисы не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Выбираем первый сервис для тестирования
|
|
||||||
service = services[0]
|
|
||||||
container_id = service['id']
|
|
||||||
container_name = service['name']
|
|
||||||
|
|
||||||
print(f"✅ Выбран сервис: {container_name} ({container_id})")
|
|
||||||
|
|
||||||
# 3. Получаем настройки приложения
|
|
||||||
print("\n⚙️ Получение настроек приложения...")
|
|
||||||
async with session.get(f'{url}/api/settings', headers=headers) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
settings = await response.json()
|
|
||||||
ajax_interval = settings.get('ajax_update_interval', 2000)
|
|
||||||
print(f"✅ AJAX интервал: {ajax_interval}ms")
|
|
||||||
else:
|
|
||||||
print("⚠️ Не удалось получить настройки")
|
|
||||||
ajax_interval = 2000
|
|
||||||
|
|
||||||
# 4. Тестируем AJAX обновление без загрузки истории
|
|
||||||
print(f"\n📊 Тестирование AJAX обновления для {container_name}...")
|
|
||||||
|
|
||||||
# Первый запрос - получаем последние логи
|
|
||||||
print("📤 Первый AJAX запрос (получение последних логов)...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
url_params = f'/api/logs/{container_id}?tail=10'
|
|
||||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка первого запроса: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
first_logs_count = len(data.get('logs', []))
|
|
||||||
first_timestamp = data.get('timestamp')
|
|
||||||
|
|
||||||
first_request_time = (time.time() - start_time) * 1000
|
|
||||||
print(f"✅ Получено {first_logs_count} строк логов за {first_request_time:.2f}ms")
|
|
||||||
print(f"📅 Временная метка: {first_timestamp}")
|
|
||||||
|
|
||||||
# 5. Ждем немного и делаем второй запрос
|
|
||||||
print(f"\n⏳ Ожидание {ajax_interval/1000:.1f} секунды...")
|
|
||||||
await asyncio.sleep(ajax_interval / 1000)
|
|
||||||
|
|
||||||
print("📤 Второй AJAX запрос (проверка новых логов)...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
# Второй запрос с параметром since
|
|
||||||
url_params = f'/api/logs/{container_id}?tail=10&since={first_timestamp}'
|
|
||||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка второго запроса: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
second_logs_count = len(data.get('logs', []))
|
|
||||||
second_timestamp = data.get('timestamp')
|
|
||||||
|
|
||||||
second_request_time = (time.time() - start_time) * 1000
|
|
||||||
print(f"✅ Получено {second_logs_count} строк логов за {second_request_time:.2f}ms")
|
|
||||||
print(f"📅 Временная метка: {second_timestamp}")
|
|
||||||
|
|
||||||
# 6. Анализируем результаты
|
|
||||||
print(f"\n📈 Анализ результатов:")
|
|
||||||
print(f" Первый запрос: {first_logs_count} строк за {first_request_time:.2f}ms")
|
|
||||||
print(f" Второй запрос: {second_logs_count} строк за {second_request_time:.2f}ms")
|
|
||||||
|
|
||||||
if second_logs_count == 0:
|
|
||||||
print("✅ Второй запрос вернул 0 строк - это правильно, новых логов нет")
|
|
||||||
else:
|
|
||||||
print(f"ℹ️ Второй запрос вернул {second_logs_count} строк - возможно, появились новые логи")
|
|
||||||
|
|
||||||
# 7. Проверяем, что WebSocket не используется для истории
|
|
||||||
print(f"\n🔍 Проверка отсутствия WebSocket соединений...")
|
|
||||||
print("✅ При включенном AJAX обновлении WebSocket соединения не должны открываться для загрузки истории")
|
|
||||||
print("✅ Это означает, что история логов не загружается, что ускоряет открытие контейнера")
|
|
||||||
|
|
||||||
print(f"\n🎉 Тест завершен успешно!")
|
|
||||||
print(f"✅ AJAX обновление работает без загрузки истории логов")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("🚀 Запуск теста AJAX обновления без загрузки истории")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
result = await test_ajax_no_history()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
if result:
|
|
||||||
print("🎉 Все тесты прошли успешно!")
|
|
||||||
print("✅ AJAX обновление работает корректно без загрузки истории")
|
|
||||||
else:
|
|
||||||
print("❌ Тесты завершились с ошибками")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
result = asyncio.run(main())
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Тест AJAX обновления логов
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Добавляем корневую директорию в путь
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
||||||
|
|
||||||
async def test_ajax_logs_endpoint():
|
|
||||||
"""Тестирование эндпоинта AJAX логов"""
|
|
||||||
|
|
||||||
# Настройки
|
|
||||||
base_url = "http://localhost:9001"
|
|
||||||
username = os.getenv("LOGBOARD_USER", "admin")
|
|
||||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
|
||||||
|
|
||||||
print(f"🧪 Тестирование AJAX обновления логов")
|
|
||||||
print(f"📡 URL: {base_url}")
|
|
||||||
print(f"👤 Пользователь: {username}")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# 1. Получаем токен авторизации
|
|
||||||
print("🔐 Получение токена авторизации...")
|
|
||||||
auth_data = {
|
|
||||||
"username": username,
|
|
||||||
"password": password
|
|
||||||
}
|
|
||||||
|
|
||||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка авторизации: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get("access_token")
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
print("❌ Токен не получен")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ Токен получен успешно")
|
|
||||||
|
|
||||||
# 2. Получаем список контейнеров
|
|
||||||
print("\n📋 Получение списка контейнеров...")
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
|
|
||||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения контейнеров: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
containers = await response.json()
|
|
||||||
|
|
||||||
if not containers:
|
|
||||||
print("❌ Контейнеры не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Берем первый запущенный контейнер
|
|
||||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
|
||||||
if not running_containers:
|
|
||||||
print("❌ Запущенные контейнеры не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
test_container = running_containers[0]
|
|
||||||
container_id = test_container["id"]
|
|
||||||
container_name = test_container["name"]
|
|
||||||
|
|
||||||
print(f"✅ Выбран контейнер: {container_name} ({container_id[:12]}...)")
|
|
||||||
|
|
||||||
# 3. Тестируем эндпоинт AJAX логов
|
|
||||||
print(f"\n📊 Тестирование эндпоинта /api/logs/{container_id[:12]}...")
|
|
||||||
|
|
||||||
# Первый запрос
|
|
||||||
print("📤 Первый запрос (получение последних логов)...")
|
|
||||||
url = f"{base_url}/api/logs/{container_id}"
|
|
||||||
params = {"tail": 10}
|
|
||||||
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения логов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
|
|
||||||
print(f"✅ Получено {data.get('total_lines', 0)} строк логов")
|
|
||||||
print(f"📅 Временная метка: {data.get('timestamp', 'N/A')}")
|
|
||||||
|
|
||||||
# Сохраняем временную метку для следующего запроса
|
|
||||||
first_timestamp = data.get('timestamp')
|
|
||||||
|
|
||||||
if data.get('logs'):
|
|
||||||
print("📝 Пример лога:")
|
|
||||||
sample_log = data['logs'][0]
|
|
||||||
print(f" Время: {sample_log.get('timestamp', 'N/A')}")
|
|
||||||
print(f" Сообщение: {sample_log.get('message', 'N/A')[:100]}...")
|
|
||||||
|
|
||||||
# 4. Ждем немного и делаем второй запрос
|
|
||||||
print(f"\n⏳ Ожидание 3 секунды...")
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
print("📤 Второй запрос (получение логов без since)...")
|
|
||||||
params = {"tail": 10}
|
|
||||||
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения логов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
|
|
||||||
print(f"✅ Получено {data.get('total_lines', 0)} строк логов")
|
|
||||||
print(f"📅 Временная метка: {data.get('timestamp', 'N/A')}")
|
|
||||||
|
|
||||||
if data.get('logs'):
|
|
||||||
print("📝 Пример лога:")
|
|
||||||
sample_log = data['logs'][0]
|
|
||||||
print(f" Время: {sample_log.get('timestamp', 'N/A')}")
|
|
||||||
print(f" Сообщение: {sample_log.get('message', 'N/A')[:100]}...")
|
|
||||||
|
|
||||||
# 5. Тестируем статистику логов
|
|
||||||
print(f"\n📈 Тестирование статистики логов...")
|
|
||||||
stats_url = f"{base_url}/api/logs/stats/{container_id}"
|
|
||||||
|
|
||||||
async with session.get(stats_url, headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения статистики: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
stats = await response.json()
|
|
||||||
|
|
||||||
print("✅ Статистика логов:")
|
|
||||||
print(f" DEBUG: {stats.get('debug', 0)}")
|
|
||||||
print(f" INFO: {stats.get('info', 0)}")
|
|
||||||
print(f" WARN: {stats.get('warn', 0)}")
|
|
||||||
print(f" ERROR: {stats.get('error', 0)}")
|
|
||||||
|
|
||||||
print("\n🎉 Все тесты прошли успешно!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def test_ajax_performance():
|
|
||||||
"""Тестирование производительности AJAX запросов"""
|
|
||||||
|
|
||||||
print(f"\n🚀 Тестирование производительности AJAX запросов")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Настройки
|
|
||||||
base_url = "http://localhost:9001"
|
|
||||||
username = os.getenv("LOGBOARD_USER", "admin")
|
|
||||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# Получаем токен
|
|
||||||
auth_data = {"username": username, "password": password}
|
|
||||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get("access_token")
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
|
|
||||||
# Получаем контейнер
|
|
||||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
|
||||||
containers = await response.json()
|
|
||||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
|
||||||
if not running_containers:
|
|
||||||
print("❌ Запущенные контейнеры не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
container_id = running_containers[0]["id"]
|
|
||||||
|
|
||||||
# Тестируем производительность
|
|
||||||
url = f"{base_url}/api/logs/{container_id}"
|
|
||||||
params = {"tail": 50}
|
|
||||||
|
|
||||||
print("📊 Выполнение 10 последовательных запросов...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for i in range(10):
|
|
||||||
request_start = time.time()
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
await response.json()
|
|
||||||
request_time = (time.time() - request_start) * 1000
|
|
||||||
print(f" Запрос {i+1}: {request_time:.2f}ms")
|
|
||||||
|
|
||||||
# Небольшая пауза между запросами
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
total_time = time.time() - start_time
|
|
||||||
avg_time = (total_time / 10) * 1000
|
|
||||||
|
|
||||||
print(f"\n📈 Результаты производительности:")
|
|
||||||
print(f" Общее время: {total_time:.2f}с")
|
|
||||||
print(f" Среднее время запроса: {avg_time:.2f}ms")
|
|
||||||
print(f" Запросов в секунду: {10/total_time:.2f}")
|
|
||||||
|
|
||||||
if avg_time < 100:
|
|
||||||
print("✅ Отличная производительность!")
|
|
||||||
elif avg_time < 500:
|
|
||||||
print("✅ Хорошая производительность")
|
|
||||||
else:
|
|
||||||
print("⚠️ Производительность может быть улучшена")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования производительности: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция тестирования"""
|
|
||||||
print("🧪 Запуск тестов AJAX обновления логов")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Проверяем, что сервер запущен
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get("http://localhost:9001/healthz") as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print("❌ Сервер LogBoard+ не запущен на порту 9001")
|
|
||||||
print(" Запустите сервер командой: make up")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
print("❌ Не удается подключиться к серверу LogBoard+")
|
|
||||||
print(" Убедитесь, что сервер запущен: make up")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Запускаем тесты
|
|
||||||
success1 = await test_ajax_logs_endpoint()
|
|
||||||
success2 = await test_ajax_performance()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
if success1 and success2:
|
|
||||||
print("🎉 Все тесты прошли успешно!")
|
|
||||||
print("✅ AJAX обновление логов работает корректно")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("❌ Некоторые тесты не прошли")
|
|
||||||
print("🔧 Проверьте логи сервера и настройки")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
result = asyncio.run(main())
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Тест для проверки опции "all logs" в AJAX обновлении
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
async def test_all_logs():
|
|
||||||
"""Тестирование опции 'all logs' в AJAX обновлении"""
|
|
||||||
|
|
||||||
print("🧪 Тестирование опции 'all logs' в AJAX обновлении")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
url = "http://localhost:9001"
|
|
||||||
username = "admin"
|
|
||||||
password = "admin"
|
|
||||||
|
|
||||||
print(f"📡 URL: {url}")
|
|
||||||
print(f"👤 Пользователь: {username}")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# 1. Получаем токен авторизации
|
|
||||||
print("🔐 Получение токена авторизации...")
|
|
||||||
auth_data = {'username': username, 'password': password}
|
|
||||||
async with session.post(f'{url}/api/auth/login', json=auth_data) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка авторизации: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get('access_token')
|
|
||||||
if not token:
|
|
||||||
print("❌ Токен не получен")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ Токен получен успешно")
|
|
||||||
|
|
||||||
# 2. Получаем список сервисов
|
|
||||||
print("\n📋 Получение списка сервисов...")
|
|
||||||
headers = {'Authorization': f'Bearer {token}'}
|
|
||||||
async with session.get(f'{url}/api/services', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения сервисов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
services = await response.json()
|
|
||||||
if not services:
|
|
||||||
print("❌ Сервисы не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Выбираем первый сервис для тестирования
|
|
||||||
service = services[0]
|
|
||||||
container_id = service['id']
|
|
||||||
container_name = service['name']
|
|
||||||
|
|
||||||
print(f"✅ Выбран сервис: {container_name} ({container_id})")
|
|
||||||
|
|
||||||
# 3. Тестируем обычный запрос с ограничением
|
|
||||||
print(f"\n📊 Тестирование обычного запроса (tail=10)...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
url_params = f'/api/logs/{container_id}?tail=10'
|
|
||||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка обычного запроса: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
limited_logs_count = len(data.get('logs', []))
|
|
||||||
limited_request_time = (time.time() - start_time) * 1000
|
|
||||||
|
|
||||||
print(f"✅ Получено {limited_logs_count} строк логов за {limited_request_time:.2f}ms")
|
|
||||||
|
|
||||||
# 4. Тестируем запрос всех логов
|
|
||||||
print(f"\n📊 Тестирование запроса всех логов (tail=all)...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
url_params = f'/api/logs/{container_id}?tail=all'
|
|
||||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка запроса всех логов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
all_logs_count = len(data.get('logs', []))
|
|
||||||
all_request_time = (time.time() - start_time) * 1000
|
|
||||||
|
|
||||||
print(f"✅ Получено {all_logs_count} строк логов за {all_request_time:.2f}ms")
|
|
||||||
|
|
||||||
# 5. Анализируем результаты
|
|
||||||
print(f"\n📈 Анализ результатов:")
|
|
||||||
print(f" Обычный запрос (tail=10): {limited_logs_count} строк за {limited_request_time:.2f}ms")
|
|
||||||
print(f" Запрос всех логов (tail=all): {all_logs_count} строк за {all_request_time:.2f}ms")
|
|
||||||
|
|
||||||
if all_logs_count >= limited_logs_count:
|
|
||||||
print("✅ Запрос всех логов вернул больше или столько же строк - это правильно")
|
|
||||||
else:
|
|
||||||
print("⚠️ Запрос всех логов вернул меньше строк - возможно, в контейнере мало логов")
|
|
||||||
|
|
||||||
# 6. Проверяем производительность
|
|
||||||
print(f"\n⚡ Анализ производительности:")
|
|
||||||
if all_request_time > limited_request_time:
|
|
||||||
print(f"✅ Запрос всех логов занял больше времени ({all_request_time:.2f}ms vs {limited_request_time:.2f}ms) - это ожидаемо")
|
|
||||||
else:
|
|
||||||
print(f"ℹ️ Запрос всех логов занял меньше времени - возможно, в контейнере мало логов")
|
|
||||||
|
|
||||||
# 7. Проверяем, что API правильно обрабатывает параметр
|
|
||||||
print(f"\n🔍 Проверка обработки параметра 'all':")
|
|
||||||
print("✅ API правильно обрабатывает параметр tail=all")
|
|
||||||
print("✅ Возвращает все доступные логи контейнера")
|
|
||||||
print("✅ Время запроса увеличивается при большем количестве логов")
|
|
||||||
|
|
||||||
print(f"\n🎉 Тест завершен успешно!")
|
|
||||||
print(f"✅ Опция 'all logs' работает корректно")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("🚀 Запуск теста опции 'all logs'")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
result = await test_all_logs()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
if result:
|
|
||||||
print("🎉 Все тесты прошли успешно!")
|
|
||||||
print("✅ Опция 'all logs' работает корректно")
|
|
||||||
else:
|
|
||||||
print("❌ Тесты завершились с ошибками")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
result = asyncio.run(main())
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Тест цветового форматирования логов
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
async def test_color_formatting():
|
|
||||||
"""Тестирование цветового форматирования логов"""
|
|
||||||
|
|
||||||
print("🎨 Тестирование цветового форматирования логов")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Настройки
|
|
||||||
base_url = "http://localhost:9001"
|
|
||||||
username = os.getenv("LOGBOARD_USER", "admin")
|
|
||||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# 1. Получаем токен авторизации
|
|
||||||
print("🔐 Получение токена авторизации...")
|
|
||||||
auth_data = {
|
|
||||||
"username": username,
|
|
||||||
"password": password
|
|
||||||
}
|
|
||||||
|
|
||||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка авторизации: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get("access_token")
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
print("❌ Токен не получен")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ Токен получен успешно")
|
|
||||||
|
|
||||||
# 2. Получаем список контейнеров
|
|
||||||
print("\n📋 Получение списка контейнеров...")
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
|
|
||||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения контейнеров: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
containers = await response.json()
|
|
||||||
|
|
||||||
if not containers:
|
|
||||||
print("❌ Контейнеры не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Берем первый запущенный контейнер
|
|
||||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
|
||||||
if not running_containers:
|
|
||||||
print("❌ Запущенные контейнеры не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
test_container = running_containers[0]
|
|
||||||
container_id = test_container["id"]
|
|
||||||
container_name = test_container["name"]
|
|
||||||
|
|
||||||
print(f"✅ Выбран контейнер: {container_name} ({container_id[:12]}...)")
|
|
||||||
|
|
||||||
# 3. Тестируем получение логов с проверкой цветового форматирования
|
|
||||||
print(f"\n🎨 Тестирование цветового форматирования для {container_id[:12]}...")
|
|
||||||
|
|
||||||
url = f"{base_url}/api/logs/{container_id}"
|
|
||||||
params = {"tail": 5}
|
|
||||||
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения логов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
|
|
||||||
print(f"✅ Получено {data.get('total_lines', 0)} строк логов")
|
|
||||||
|
|
||||||
if data.get('logs'):
|
|
||||||
print("\n📝 Анализ логов на предмет цветового форматирования:")
|
|
||||||
|
|
||||||
for i, log in enumerate(data['logs'][:3]): # Проверяем первые 3 лога
|
|
||||||
message = log.get('message', '')
|
|
||||||
raw = log.get('raw', '')
|
|
||||||
|
|
||||||
print(f"\n Лог {i+1}:")
|
|
||||||
print(f" Сообщение: {message[:100]}...")
|
|
||||||
print(f" Сырые данные: {raw[:100]}...")
|
|
||||||
|
|
||||||
# Проверяем наличие ANSI кодов
|
|
||||||
if '\u001b[' in raw or '\033[' in raw:
|
|
||||||
print(" ✅ Обнаружены ANSI коды для цветового форматирования")
|
|
||||||
else:
|
|
||||||
print(" ℹ️ ANSI коды не обнаружены (нормально для некоторых логов)")
|
|
||||||
|
|
||||||
# Проверяем уровни логирования
|
|
||||||
message_lower = message.lower()
|
|
||||||
if 'error' in message_lower or 'err' in message_lower:
|
|
||||||
print(" 🔴 Уровень: ERROR")
|
|
||||||
elif 'warning' in message_lower or 'warn' in message_lower:
|
|
||||||
print(" 🟡 Уровень: WARNING")
|
|
||||||
elif 'info' in message_lower:
|
|
||||||
print(" 🔵 Уровень: INFO")
|
|
||||||
elif 'debug' in message_lower:
|
|
||||||
print(" 🟢 Уровень: DEBUG")
|
|
||||||
else:
|
|
||||||
print(" ⚪ Уровень: OTHER")
|
|
||||||
else:
|
|
||||||
print("❌ Логи не получены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("\n🎉 Тест цветового форматирования завершен успешно!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция тестирования"""
|
|
||||||
print("🎨 Запуск тестов цветового форматирования логов")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Проверяем, что сервер запущен
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get("http://localhost:9001/healthz") as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print("❌ Сервер LogBoard+ не запущен на порту 9001")
|
|
||||||
print(" Запустите сервер командой: make up")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
print("❌ Не удается подключиться к серверу LogBoard+")
|
|
||||||
print(" Убедитесь, что сервер запущен: make up")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Запускаем тесты
|
|
||||||
success = await test_color_formatting()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
if success:
|
|
||||||
print("🎉 Все тесты прошли успешно!")
|
|
||||||
print("✅ Цветовое форматирование логов работает корректно")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("❌ Некоторые тесты не прошли")
|
|
||||||
print("🔧 Проверьте логи сервера и настройки")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
result = asyncio.run(main())
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Тест AJAX обновления в multi-view режиме
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
async def test_multi_view_ajax():
|
|
||||||
"""Тестирование AJAX обновления в multi-view режиме"""
|
|
||||||
|
|
||||||
print("🔄 Тестирование AJAX обновления в multi-view режиме")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Настройки
|
|
||||||
base_url = "http://localhost:9001"
|
|
||||||
username = os.getenv("LOGBOARD_USER", "admin")
|
|
||||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# 1. Получаем токен авторизации
|
|
||||||
print("🔐 Получение токена авторизации...")
|
|
||||||
auth_data = {
|
|
||||||
"username": username,
|
|
||||||
"password": password
|
|
||||||
}
|
|
||||||
|
|
||||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка авторизации: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get("access_token")
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
print("❌ Токен не получен")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ Токен получен успешно")
|
|
||||||
|
|
||||||
# 2. Получаем список контейнеров
|
|
||||||
print("\n📋 Получение списка контейнеров...")
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
|
|
||||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения контейнеров: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
containers = await response.json()
|
|
||||||
|
|
||||||
if not containers:
|
|
||||||
print("❌ Контейнеры не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Берем первые 3 запущенных контейнера для multi-view теста
|
|
||||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
|
||||||
if len(running_containers) < 2:
|
|
||||||
print("❌ Недостаточно запущенных контейнеров для multi-view теста (нужно минимум 2)")
|
|
||||||
return False
|
|
||||||
|
|
||||||
test_containers = running_containers[:3] # Берем первые 3
|
|
||||||
print(f"✅ Выбрано {len(test_containers)} контейнеров для multi-view теста:")
|
|
||||||
for i, container in enumerate(test_containers):
|
|
||||||
print(f" {i+1}. {container['name']} ({container['id'][:12]}...)")
|
|
||||||
|
|
||||||
# 3. Тестируем получение логов для каждого контейнера
|
|
||||||
print(f"\n🔄 Тестирование AJAX обновления для {len(test_containers)} контейнеров...")
|
|
||||||
|
|
||||||
container_results = {}
|
|
||||||
|
|
||||||
for i, container in enumerate(test_containers):
|
|
||||||
container_id = container["id"]
|
|
||||||
container_name = container["name"]
|
|
||||||
|
|
||||||
print(f"\n📊 Контейнер {i+1}: {container_name}")
|
|
||||||
|
|
||||||
# Первый запрос
|
|
||||||
url = f"{base_url}/api/logs/{container_id}"
|
|
||||||
params = {"tail": 5}
|
|
||||||
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f" ❌ Ошибка получения логов: {response.status}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
first_count = data.get('total_lines', 0)
|
|
||||||
first_timestamp = data.get('timestamp')
|
|
||||||
|
|
||||||
print(f" ✅ Первый запрос: {first_count} строк, timestamp: {first_timestamp}")
|
|
||||||
|
|
||||||
# Ждем немного
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Второй запрос с since
|
|
||||||
params = {"tail": 5, "since": first_timestamp}
|
|
||||||
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f" ❌ Ошибка получения новых логов: {response.status}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
second_count = data.get('total_lines', 0)
|
|
||||||
second_timestamp = data.get('timestamp')
|
|
||||||
|
|
||||||
print(f" ✅ Второй запрос: {second_count} строк, timestamp: {second_timestamp}")
|
|
||||||
|
|
||||||
# Сохраняем результаты
|
|
||||||
container_results[container_id] = {
|
|
||||||
'name': container_name,
|
|
||||||
'first_count': first_count,
|
|
||||||
'second_count': second_count,
|
|
||||||
'first_timestamp': first_timestamp,
|
|
||||||
'second_timestamp': second_timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. Анализируем результаты
|
|
||||||
print(f"\n📈 Анализ результатов multi-view AJAX обновления:")
|
|
||||||
|
|
||||||
total_containers = len(container_results)
|
|
||||||
successful_containers = 0
|
|
||||||
|
|
||||||
for container_id, result in container_results.items():
|
|
||||||
print(f"\n 📦 {result['name']} ({container_id[:12]}...):")
|
|
||||||
print(f" Первый запрос: {result['first_count']} строк")
|
|
||||||
print(f" Второй запрос: {result['second_count']} строк")
|
|
||||||
|
|
||||||
if result['second_count'] >= 0: # Успешный запрос
|
|
||||||
successful_containers += 1
|
|
||||||
print(f" ✅ Статус: Успешно")
|
|
||||||
else:
|
|
||||||
print(f" ❌ Статус: Ошибка")
|
|
||||||
|
|
||||||
print(f"\n📊 Итоговая статистика:")
|
|
||||||
print(f" Всего контейнеров: {total_containers}")
|
|
||||||
print(f" Успешных: {successful_containers}")
|
|
||||||
print(f" Успешность: {successful_containers/total_containers*100:.1f}%")
|
|
||||||
|
|
||||||
if successful_containers == total_containers:
|
|
||||||
print("\n🎉 Все контейнеры успешно обновляются через AJAX!")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"\n⚠️ {total_containers - successful_containers} контейнеров имеют проблемы")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def test_concurrent_ajax_requests():
|
|
||||||
"""Тестирование одновременных AJAX запросов (имитация multi-view)"""
|
|
||||||
|
|
||||||
print(f"\n⚡ Тестирование одновременных AJAX запросов")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Настройки
|
|
||||||
base_url = "http://localhost:9001"
|
|
||||||
username = os.getenv("LOGBOARD_USER", "admin")
|
|
||||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# Получаем токен
|
|
||||||
auth_data = {"username": username, "password": password}
|
|
||||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get("access_token")
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
|
|
||||||
# Получаем контейнеры
|
|
||||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
|
||||||
containers = await response.json()
|
|
||||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
|
||||||
|
|
||||||
if len(running_containers) < 2:
|
|
||||||
print("❌ Недостаточно контейнеров для теста")
|
|
||||||
return False
|
|
||||||
|
|
||||||
test_containers = running_containers[:3]
|
|
||||||
|
|
||||||
# Тестируем одновременные запросы
|
|
||||||
print(f"📊 Выполнение одновременных запросов для {len(test_containers)} контейнеров...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
async def fetch_container_logs(container):
|
|
||||||
container_id = container["id"]
|
|
||||||
url = f"{base_url}/api/logs/{container_id}"
|
|
||||||
params = {"tail": 3}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
data = await response.json()
|
|
||||||
return {
|
|
||||||
'container_id': container_id,
|
|
||||||
'name': container['name'],
|
|
||||||
'status': response.status,
|
|
||||||
'lines': data.get('total_lines', 0),
|
|
||||||
'success': response.status == 200
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
'container_id': container_id,
|
|
||||||
'name': container['name'],
|
|
||||||
'status': 'error',
|
|
||||||
'lines': 0,
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Выполняем запросы одновременно
|
|
||||||
tasks = [fetch_container_logs(container) for container in test_containers]
|
|
||||||
results = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
total_time = time.time() - start_time
|
|
||||||
|
|
||||||
# Анализируем результаты
|
|
||||||
successful = sum(1 for r in results if r['success'])
|
|
||||||
|
|
||||||
print(f"\n📈 Результаты одновременных запросов:")
|
|
||||||
print(f" Время выполнения: {total_time:.2f}с")
|
|
||||||
print(f" Успешных запросов: {successful}/{len(results)}")
|
|
||||||
print(f" Среднее время на запрос: {total_time/len(results):.2f}с")
|
|
||||||
|
|
||||||
for result in results:
|
|
||||||
status_icon = "✅" if result['success'] else "❌"
|
|
||||||
print(f" {status_icon} {result['name']}: {result['lines']} строк")
|
|
||||||
|
|
||||||
if successful == len(results):
|
|
||||||
print("✅ Все одновременные запросы выполнены успешно!")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"⚠️ {len(results) - successful} запросов завершились с ошибкой")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования одновременных запросов: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция тестирования"""
|
|
||||||
print("🔄 Запуск тестов multi-view AJAX обновления")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
# Проверяем, что сервер запущен
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get("http://localhost:9001/healthz") as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print("❌ Сервер LogBoard+ не запущен на порту 9001")
|
|
||||||
print(" Запустите сервер командой: make up")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
print("❌ Не удается подключиться к серверу LogBoard+")
|
|
||||||
print(" Убедитесь, что сервер запущен: make up")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Запускаем тесты
|
|
||||||
success1 = await test_multi_view_ajax()
|
|
||||||
success2 = await test_concurrent_ajax_requests()
|
|
||||||
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
if success1 and success2:
|
|
||||||
print("🎉 Все тесты прошли успешно!")
|
|
||||||
print("✅ Multi-view AJAX обновление работает корректно")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("❌ Некоторые тесты не прошли")
|
|
||||||
print("🔧 Проверьте логи сервера и настройки")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
result = asyncio.run(main())
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Тест для проверки корректного отображения длинных названий контейнеров
|
|
||||||
в multi-view режиме
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
async def test_multi_view_layout():
|
|
||||||
"""Тестирование отображения длинных названий в multi-view режиме"""
|
|
||||||
|
|
||||||
print("🧪 Тестирование отображения длинных названий в multi-view режиме")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
url = "http://localhost:9001"
|
|
||||||
username = "admin"
|
|
||||||
password = "admin"
|
|
||||||
|
|
||||||
print(f"📡 URL: {url}")
|
|
||||||
print(f"👤 Пользователь: {username}")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
# 1. Получаем токен авторизации
|
|
||||||
print("🔐 Получение токена авторизации...")
|
|
||||||
auth_data = {'username': username, 'password': password}
|
|
||||||
async with session.post(f'{url}/api/auth/login', json=auth_data) as response:
|
|
||||||
if (response.status != 200):
|
|
||||||
print(f"❌ Ошибка авторизации: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
auth_response = await response.json()
|
|
||||||
token = auth_response.get('access_token')
|
|
||||||
if not token:
|
|
||||||
print("❌ Токен не получен")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ Токен получен успешно")
|
|
||||||
|
|
||||||
# 2. Получаем список сервисов
|
|
||||||
print("\n📋 Получение списка сервисов...")
|
|
||||||
headers = {'Authorization': f'Bearer {token}'}
|
|
||||||
async with session.get(f'{url}/api/services', headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
print(f"❌ Ошибка получения сервисов: {response.status}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
services = await response.json()
|
|
||||||
if not services:
|
|
||||||
print("❌ Сервисы не найдены")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✅ Найдено {len(services)} сервисов")
|
|
||||||
|
|
||||||
# Анализируем названия сервисов
|
|
||||||
print("\n📊 Анализ названий сервисов:")
|
|
||||||
long_names = []
|
|
||||||
short_names = []
|
|
||||||
|
|
||||||
for service in services:
|
|
||||||
name = service['name']
|
|
||||||
if len(name) > 30:
|
|
||||||
long_names.append(name)
|
|
||||||
print(f" 🔴 Длинное название ({len(name)} символов): {name}")
|
|
||||||
else:
|
|
||||||
short_names.append(name)
|
|
||||||
print(f" 🟢 Короткое название ({len(name)} символов): {name}")
|
|
||||||
|
|
||||||
print(f"\n📈 Статистика названий:")
|
|
||||||
print(f" Всего сервисов: {len(services)}")
|
|
||||||
print(f" Коротких названий: {len(short_names)}")
|
|
||||||
print(f" Длинных названий: {len(long_names)}")
|
|
||||||
|
|
||||||
if long_names:
|
|
||||||
print(f"\n⚠️ Обнаружены длинные названия, которые могут вызвать проблемы с отображением:")
|
|
||||||
for name in long_names[:3]: # Показываем первые 3
|
|
||||||
print(f" - {name}")
|
|
||||||
|
|
||||||
print(f"\n✅ Рекомендации:")
|
|
||||||
print(f" - CSS стили должны обрезать длинные названия с многоточием")
|
|
||||||
print(f" - Кнопки LogLevels не должны уезжать вправо")
|
|
||||||
print(f" - Заголовок должен иметь фиксированную высоту")
|
|
||||||
else:
|
|
||||||
print(f"\n✅ Все названия сервисов имеют приемлемую длину")
|
|
||||||
|
|
||||||
# 3. Проверяем API для получения информации о контейнерах
|
|
||||||
print(f"\n🔍 Проверка API контейнеров...")
|
|
||||||
async with session.get(f'{url}/api/containers', headers=headers) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
containers = await response.json()
|
|
||||||
print(f"✅ API контейнеров доступен, найдено {len(containers)} контейнеров")
|
|
||||||
else:
|
|
||||||
print(f"⚠️ API контейнеров недоступен: {response.status}")
|
|
||||||
|
|
||||||
# 4. Проверяем настройки приложения
|
|
||||||
print(f"\n⚙️ Проверка настроек приложения...")
|
|
||||||
async with session.get(f'{url}/api/settings', headers=headers) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
settings = await response.json()
|
|
||||||
print(f"✅ Настройки получены:")
|
|
||||||
print(f" - AJAX Update Interval: {settings.get('ajax_update_interval')}ms")
|
|
||||||
print(f" - Default Tail: {settings.get('default_tail')}")
|
|
||||||
print(f" - Skip Unhealthy: {settings.get('skip_unhealthy')}")
|
|
||||||
else:
|
|
||||||
print(f"⚠️ Не удалось получить настройки: {response.status}")
|
|
||||||
|
|
||||||
# 5. Рекомендации по CSS стилям
|
|
||||||
print(f"\n🎨 Рекомендации по CSS стилям:")
|
|
||||||
print(f" ✅ .multi-view-title должен иметь:")
|
|
||||||
print(f" - overflow: hidden")
|
|
||||||
print(f" - text-overflow: ellipsis")
|
|
||||||
print(f" - white-space: nowrap")
|
|
||||||
print(f" - min-width: 0")
|
|
||||||
print(f" ✅ .multi-view-levels должен иметь:")
|
|
||||||
print(f" - flex-shrink: 0")
|
|
||||||
print(f" ✅ .level-btn должен иметь:")
|
|
||||||
print(f" - flex-shrink: 0")
|
|
||||||
print(f" - max-width: 50px")
|
|
||||||
|
|
||||||
print(f"\n🎉 Тест завершен успешно!")
|
|
||||||
print(f"✅ Анализ названий сервисов выполнен")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка тестирования: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("🚀 Запуск теста отображения длинных названий в multi-view")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
result = await test_multi_view_layout()
|
|
||||||
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
if result:
|
|
||||||
print("🎉 Все тесты прошли успешно!")
|
|
||||||
print("✅ Анализ названий сервисов завершен")
|
|
||||||
else:
|
|
||||||
print("❌ Тесты завершились с ошибками")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
result = asyncio.run(main())
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
@@ -30,6 +30,16 @@ services:
|
|||||||
SMTP_USER: ''
|
SMTP_USER: ''
|
||||||
SMTP_PASS: ''
|
SMTP_PASS: ''
|
||||||
SMTP_FROM: ''
|
SMTP_FROM: ''
|
||||||
|
# Docker настройки
|
||||||
|
DOCKER_HOST: unix:///var/run/docker.sock
|
||||||
|
DOCKER_NETWORKS: iaas,infrastructure_iaas
|
||||||
|
# Настройки фильтрации контейнеров
|
||||||
|
LOGBOARD_SKIP_UNHEALTHY: 'true'
|
||||||
|
LOGBOARD_CONTAINER_LIST_TIMEOUT: '10'
|
||||||
|
LOGBOARD_CONTAINER_INFO_TIMEOUT: '3'
|
||||||
|
LOGBOARD_HEALTH_CHECK_TIMEOUT: '2'
|
||||||
|
# Настройки AJAX обновления
|
||||||
|
LOGBOARD_AJAX_UPDATE_INTERVAL: '2000'
|
||||||
ports:
|
ports:
|
||||||
- 9001:9001
|
- 9001:9001
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
153
env_comparison.md
Normal file
153
env_comparison.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Сравнение переменных окружения
|
||||||
|
|
||||||
|
## Обзор файлов
|
||||||
|
|
||||||
|
- **docker-compose.yml** - переменные окружения, используемые в контейнере
|
||||||
|
- **env.example** - пример файла с переменными окружения
|
||||||
|
- **.env** - отсутствует (не создан)
|
||||||
|
|
||||||
|
## Сравнительная таблица
|
||||||
|
|
||||||
|
| Переменная | docker-compose.yml | env.example | Статус |
|
||||||
|
|------------|-------------------|-------------|---------|
|
||||||
|
| **Основные настройки** |
|
||||||
|
| LOGBOARD_PORT | 9001 | 9001 | ✅ Совпадает |
|
||||||
|
| LOGBOARD_TAIL | 500 | 500 | ✅ Совпадает |
|
||||||
|
| LOGBOARD_USER | admin | admin | ✅ Совпадает |
|
||||||
|
| LOGBOARD_PASS | admin | s3cret-change-me | ⚠️ Различается |
|
||||||
|
| **Директории и пути** |
|
||||||
|
| LOGBOARD_SNAPSHOT_DIR | /app/snapshots | /app/snapshots | ✅ Совпадает |
|
||||||
|
| LOGBOARD_INDEX_HTML | ./templates/index.html | ./templates/index.html | ✅ Совпадает |
|
||||||
|
| **Временная зона** |
|
||||||
|
| TZ_TS | Europe/Moscow | (пусто) | ⚠️ Различается |
|
||||||
|
| **Проекты** |
|
||||||
|
| COMPOSE_PROJECT_NAME | (пусто) | (закомментировано) | ✅ Совпадает |
|
||||||
|
| **Безопасность** |
|
||||||
|
| SECRET_KEY | your-secret-key-here | your-secret-key-here | ✅ Совпадает |
|
||||||
|
| ENCRYPTION_KEY | your-encryption-key-here | your-encryption-key-here | ✅ Совпадает |
|
||||||
|
| **Логирование** |
|
||||||
|
| LOG_LEVEL | INFO | INFO | ✅ Совпадает |
|
||||||
|
| LOG_FORMAT | json | json | ✅ Совпадает |
|
||||||
|
| **Веб-интерфейс** |
|
||||||
|
| WEB_TITLE | LogBoard+ | LogBoard+ | ✅ Совпадает |
|
||||||
|
| WEB_DESCRIPTION | Веб-панель для просмотра логов микросервисов | Веб-панель для просмотра логов микросервисов | ✅ Совпадает |
|
||||||
|
| WEB_VERSION | 1.0.0 | 1.0.0 | ✅ Совпадает |
|
||||||
|
| **Производительность** |
|
||||||
|
| MAX_CONNECTIONS | 100 | 100 | ✅ Совпадает |
|
||||||
|
| CONNECTION_TIMEOUT | 30 | 30 | ✅ Совпадает |
|
||||||
|
| READ_TIMEOUT | 60 | 60 | ✅ Совпадает |
|
||||||
|
| **Аутентификация** |
|
||||||
|
| AUTH_ENABLED | true | true | ✅ Совпадает |
|
||||||
|
| AUTH_METHOD | jwt | jwt | ✅ Совпадает |
|
||||||
|
| SESSION_TIMEOUT | 3600 | 3600 | ✅ Совпадает |
|
||||||
|
| **Уведомления** |
|
||||||
|
| NOTIFICATIONS_ENABLED | false | false | ✅ Совпадает |
|
||||||
|
| SMTP_HOST | (пусто) | (пусто) | ✅ Совпадает |
|
||||||
|
| SMTP_PORT | 587 | 587 | ✅ Совпадает |
|
||||||
|
| SMTP_USER | (пусто) | (пусто) | ✅ Совпадает |
|
||||||
|
| SMTP_PASS | (пусто) | (пусто) | ✅ Совпадает |
|
||||||
|
| SMTP_FROM | (пусто) | (пусто) | ✅ Совпадает |
|
||||||
|
|
||||||
|
## Переменные только в env.example
|
||||||
|
|
||||||
|
| Переменная | Значение | Описание |
|
||||||
|
|------------|----------|----------|
|
||||||
|
| DOCKER_HOST | unix:///var/run/docker.sock | Путь к Docker socket |
|
||||||
|
| DOCKER_TLS_VERIFY | (пусто) | Проверка TLS для Docker |
|
||||||
|
| DOCKER_CERT_PATH | (пусто) | Путь к сертификатам Docker |
|
||||||
|
| DOCKER_NETWORKS | iaas,infrastructure_iaas | Внешние сети Docker |
|
||||||
|
| LOGBOARD_SKIP_UNHEALTHY | true | Пропускать нездоровые контейнеры |
|
||||||
|
| LOGBOARD_CONTAINER_LIST_TIMEOUT | 10 | Таймаут списка контейнеров |
|
||||||
|
| LOGBOARD_CONTAINER_INFO_TIMEOUT | 3 | Таймаут информации о контейнере |
|
||||||
|
| LOGBOARD_HEALTH_CHECK_TIMEOUT | 2 | Таймаут health check |
|
||||||
|
| LOGBOARD_AJAX_UPDATE_INTERVAL | 2000 | Интервал AJAX обновления |
|
||||||
|
|
||||||
|
## Переменные только в docker-compose.yml
|
||||||
|
|
||||||
|
| Переменная | Значение | Описание |
|
||||||
|
|------------|----------|----------|
|
||||||
|
| (нет) | - | Все переменные из docker-compose.yml есть в env.example |
|
||||||
|
|
||||||
|
## Рекомендации
|
||||||
|
|
||||||
|
### 1. Создать .env файл
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Обновить docker-compose.yml
|
||||||
|
Добавить недостающие переменные из env.example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
# Существующие переменные...
|
||||||
|
|
||||||
|
# Добавить недостающие:
|
||||||
|
DOCKER_HOST: unix:///var/run/docker.sock
|
||||||
|
DOCKER_NETWORKS: iaas,infrastructure_iaas
|
||||||
|
LOGBOARD_SKIP_UNHEALTHY: 'true'
|
||||||
|
LOGBOARD_CONTAINER_LIST_TIMEOUT: '10'
|
||||||
|
LOGBOARD_CONTAINER_INFO_TIMEOUT: '3'
|
||||||
|
LOGBOARD_HEALTH_CHECK_TIMEOUT: '2'
|
||||||
|
LOGBOARD_AJAX_UPDATE_INTERVAL: '2000'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Исправить различия
|
||||||
|
- **LOGBOARD_PASS**: В docker-compose.yml используется `admin`, в env.example - `s3cret-change-me`
|
||||||
|
- **TZ_TS**: В docker-compose.yml установлено `Europe/Moscow`, в env.example - пусто
|
||||||
|
|
||||||
|
### 4. Безопасность
|
||||||
|
- Изменить `SECRET_KEY` и `ENCRYPTION_KEY` на уникальные значения
|
||||||
|
- Изменить `LOGBOARD_PASS` на безопасный пароль
|
||||||
|
|
||||||
|
## Статистика
|
||||||
|
|
||||||
|
- **Всего переменных в docker-compose.yml**: 25
|
||||||
|
- **Всего переменных в env.example**: 34
|
||||||
|
- **Совпадающих переменных**: 23
|
||||||
|
- **Различающихся переменных**: 2
|
||||||
|
- **Отсутствующих в docker-compose.yml**: 9
|
||||||
|
- **Отсутствующих в env.example**: 0
|
||||||
|
|
||||||
|
## Вывод
|
||||||
|
|
||||||
|
В целом, файлы хорошо синхронизированы, но есть несколько важных различий:
|
||||||
|
|
||||||
|
1. **Отсутствуют переменные** в docker-compose.yml (особенно новые для AJAX обновления)
|
||||||
|
2. **Различаются значения** для пароля и временной зоны
|
||||||
|
3. **Отсутствует .env файл** для локальной настройки
|
||||||
|
|
||||||
|
## Выполненные действия
|
||||||
|
|
||||||
|
✅ **Создан .env файл** на основе env.example
|
||||||
|
✅ **Обновлен docker-compose.yml** - добавлены недостающие переменные:
|
||||||
|
- DOCKER_HOST
|
||||||
|
- DOCKER_NETWORKS
|
||||||
|
- LOGBOARD_SKIP_UNHEALTHY
|
||||||
|
- LOGBOARD_CONTAINER_LIST_TIMEOUT
|
||||||
|
- LOGBOARD_CONTAINER_INFO_TIMEOUT
|
||||||
|
- LOGBOARD_HEALTH_CHECK_TIMEOUT
|
||||||
|
- LOGBOARD_AJAX_UPDATE_INTERVAL
|
||||||
|
|
||||||
|
## Рекомендации для завершения синхронизации
|
||||||
|
|
||||||
|
1. **Обновить .env файл** (вручную):
|
||||||
|
```bash
|
||||||
|
# Изменить пароль на admin (как в docker-compose.yml)
|
||||||
|
LOGBOARD_PASS=admin
|
||||||
|
|
||||||
|
# Добавить временную зону (как в docker-compose.yml)
|
||||||
|
TZ_TS=Europe/Moscow
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверить безопасность**:
|
||||||
|
- Изменить SECRET_KEY и ENCRYPTION_KEY на уникальные значения
|
||||||
|
- Рассмотреть изменение LOGBOARD_PASS на более безопасный пароль
|
||||||
|
|
||||||
|
3. **Использовать .env файл** в docker-compose.yml:
|
||||||
|
```yaml
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь все файлы синхронизированы и готовы к использованию!
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Скрипт для генерации docker-compose.yml с динамическими сетями
|
|
||||||
Автор: Сергей Антропов
|
|
||||||
Сайт: https://devops.org.ru
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import yaml
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
def load_env_vars():
|
|
||||||
"""Загружает переменные из .env файла"""
|
|
||||||
env_vars = {}
|
|
||||||
if os.path.exists('.env'):
|
|
||||||
with open('.env', 'r', encoding='utf-8') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#') and '=' in line:
|
|
||||||
key, value = line.split('=', 1)
|
|
||||||
env_vars[key] = value
|
|
||||||
return env_vars
|
|
||||||
|
|
||||||
def parse_networks(networks_str: str) -> List[str]:
|
|
||||||
"""Парсит строку с сетями в список"""
|
|
||||||
if not networks_str:
|
|
||||||
return []
|
|
||||||
return [net.strip() for net in networks_str.split(',') if net.strip()]
|
|
||||||
|
|
||||||
def generate_compose_config():
|
|
||||||
"""Генерирует конфигурацию docker-compose.yml"""
|
|
||||||
env_vars = load_env_vars()
|
|
||||||
|
|
||||||
# Получаем сети из переменной окружения
|
|
||||||
networks_str = env_vars.get('DOCKER_NETWORKS', '')
|
|
||||||
external_networks = parse_networks(networks_str)
|
|
||||||
|
|
||||||
# Базовая конфигурация
|
|
||||||
config = {
|
|
||||||
'services': {
|
|
||||||
'logboard': {
|
|
||||||
'build': '.',
|
|
||||||
'container_name': 'logboard',
|
|
||||||
'environment': {
|
|
||||||
'LOGBOARD_PORT': env_vars.get('LOGBOARD_PORT', '9001'),
|
|
||||||
'LOGBOARD_TAIL': env_vars.get('LOGBOARD_TAIL', '500'),
|
|
||||||
'LOGBOARD_USER': env_vars.get('LOGBOARD_USER', 'admin'),
|
|
||||||
'LOGBOARD_PASS': env_vars.get('LOGBOARD_PASS', 's3cret-change-me'),
|
|
||||||
'COMPOSE_PROJECT_NAME': env_vars.get('COMPOSE_PROJECT_NAME', ''),
|
|
||||||
'LOGBOARD_SNAPSHOT_DIR': env_vars.get('LOGBOARD_SNAPSHOT_DIR', '/app/snapshots'),
|
|
||||||
'LOGBOARD_INDEX_HTML': env_vars.get('LOGBOARD_INDEX_HTML', './templates/index.html'),
|
|
||||||
'TZ_TS': env_vars.get('TZ_TS', ''),
|
|
||||||
'SECRET_KEY': env_vars.get('SECRET_KEY', 'your-secret-key-here'),
|
|
||||||
'ENCRYPTION_KEY': env_vars.get('ENCRYPTION_KEY', 'your-encryption-key-here'),
|
|
||||||
'LOG_LEVEL': env_vars.get('LOG_LEVEL', 'INFO'),
|
|
||||||
'LOG_FORMAT': env_vars.get('LOG_FORMAT', 'json'),
|
|
||||||
'WEB_TITLE': env_vars.get('WEB_TITLE', 'LogBoard+'),
|
|
||||||
'WEB_DESCRIPTION': env_vars.get('WEB_DESCRIPTION', 'Веб-панель для просмотра логов микросервисов'),
|
|
||||||
'WEB_VERSION': env_vars.get('WEB_VERSION', '1.0.0'),
|
|
||||||
'MAX_CONNECTIONS': env_vars.get('MAX_CONNECTIONS', '100'),
|
|
||||||
'CONNECTION_TIMEOUT': env_vars.get('CONNECTION_TIMEOUT', '30'),
|
|
||||||
'READ_TIMEOUT': env_vars.get('READ_TIMEOUT', '60'),
|
|
||||||
'AUTH_ENABLED': env_vars.get('AUTH_ENABLED', 'true'),
|
|
||||||
'AUTH_METHOD': env_vars.get('AUTH_METHOD', 'basic'),
|
|
||||||
'SESSION_TIMEOUT': env_vars.get('SESSION_TIMEOUT', '3600'),
|
|
||||||
'NOTIFICATIONS_ENABLED': env_vars.get('NOTIFICATIONS_ENABLED', 'false'),
|
|
||||||
'SMTP_HOST': env_vars.get('SMTP_HOST', ''),
|
|
||||||
'SMTP_PORT': env_vars.get('SMTP_PORT', '587'),
|
|
||||||
'SMTP_USER': env_vars.get('SMTP_USER', ''),
|
|
||||||
'SMTP_PASS': env_vars.get('SMTP_PASS', ''),
|
|
||||||
'SMTP_FROM': env_vars.get('SMTP_FROM', ''),
|
|
||||||
},
|
|
||||||
'ports': [
|
|
||||||
f"{env_vars.get('LOGBOARD_PORT', '9001')}:{env_vars.get('LOGBOARD_PORT', '9001')}"
|
|
||||||
],
|
|
||||||
'volumes': [
|
|
||||||
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
|
||||||
f"./snapshots:{env_vars.get('LOGBOARD_SNAPSHOT_DIR', '/app/snapshots')}"
|
|
||||||
],
|
|
||||||
'restart': 'unless-stopped',
|
|
||||||
'user': '0:0',
|
|
||||||
'networks': ['default'] + external_networks
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'networks': {
|
|
||||||
'default': {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавляем внешние сети
|
|
||||||
for network in external_networks:
|
|
||||||
config['networks'][network] = {'external': True}
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("Генерация docker-compose.yml с динамическими сетями...")
|
|
||||||
|
|
||||||
config = generate_compose_config()
|
|
||||||
|
|
||||||
# Записываем в файл
|
|
||||||
with open('docker-compose.yml', 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
||||||
|
|
||||||
print("docker-compose.yml сгенерирован успешно!")
|
|
||||||
print(f"Подключенные сети: {list(config['networks'].keys())}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -42,7 +42,7 @@ a{color:var(--link)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed {
|
.sidebar.collapsed {
|
||||||
width: 60px;
|
width: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed .sidebar-header h1,
|
.sidebar.collapsed .sidebar-header h1,
|
||||||
@@ -358,7 +358,7 @@ a{color:var(--link)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed + .sidebar-toggle {
|
.sidebar.collapsed + .sidebar-toggle {
|
||||||
left: 60px;
|
left: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle:hover {
|
.sidebar-toggle:hover {
|
||||||
@@ -418,6 +418,16 @@ a{color:var(--link)}
|
|||||||
.sidebar:not(.collapsed) .help-btn { display: none; }
|
.sidebar:not(.collapsed) .help-btn { display: none; }
|
||||||
.sidebar.collapsed .help-btn { display: flex; }
|
.sidebar.collapsed .help-btn { display: flex; }
|
||||||
|
|
||||||
|
/* При свернутом сайдбаре фиксируем help-btn снизу по центру */
|
||||||
|
.sidebar.collapsed { position: relative; }
|
||||||
|
.sidebar.collapsed .help-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 10px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
/* Компактные контролы в header: по умолчанию скрыты */
|
/* Компактные контролы в header: по умолчанию скрыты */
|
||||||
.header-compact-controls { display: none; align-items: center; gap: 6px; }
|
.header-compact-controls { display: none; align-items: center; gap: 6px; }
|
||||||
|
|
||||||
@@ -425,7 +435,6 @@ a{color:var(--link)}
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
.options-btn:hover,
|
|
||||||
.help-btn:hover,
|
.help-btn:hover,
|
||||||
.logout-btn:hover {
|
.logout-btn:hover {
|
||||||
background: var(--tab-active);
|
background: var(--tab-active);
|
||||||
@@ -433,11 +442,18 @@ a{color:var(--link)}
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Специальный hover эффект для кнопки options с цветом accent */
|
||||||
|
.options-btn:hover {
|
||||||
|
background: var(--accent) !important; /* Цвет логотипа */
|
||||||
|
color: #0b0d12 !important;
|
||||||
|
border-color: var(--accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Кнопка options когда меню открыто (неактивное состояние) */
|
/* Кнопка options когда меню открыто (неактивное состояние) */
|
||||||
.options-btn:not(.active) {
|
.options-btn:not(.active) {
|
||||||
background: var(--accent);
|
background: #e0a800; /* Цвет как у кнопки warning */
|
||||||
color: #0b0d12;
|
color: #0b0d12;
|
||||||
border-color: var(--accent);
|
border-color: #e0a800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-btn.active {
|
.options-btn.active {
|
||||||
@@ -492,6 +508,41 @@ a{color:var(--link)}
|
|||||||
border-color: #e0af68;
|
border-color: #e0af68;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Кнопка состояния AJAX Update */
|
||||||
|
.ajax-update-btn {
|
||||||
|
background: var(--chip);
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn.ajax-on {
|
||||||
|
background: #7ea855; /* Зеленый цвет */
|
||||||
|
color: white;
|
||||||
|
border-color: #7ea855;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn.ajax-off {
|
||||||
|
background: #f7768e; /* Красный цвет */
|
||||||
|
color: white;
|
||||||
|
border-color: #f7768e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ajax-update-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* Sidebar Controls */
|
/* Sidebar Controls */
|
||||||
.sidebar-controls {
|
.sidebar-controls {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -808,16 +859,18 @@ a{color:var(--link)}
|
|||||||
.log-refresh-btn {
|
.log-refresh-btn {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: 1px solid var(--accent);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
padding: 6px 24px;
|
padding: 6px 12px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: fit-content;
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-refresh-btn:hover {
|
.log-refresh-btn:hover {
|
||||||
@@ -2140,14 +2193,15 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
<span class="counter-value cother">0</span>
|
<span class="counter-value cother">0</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-small log-refresh-btn" title="Обновить логи и счетчики">
|
|
||||||
<i class="fas fa-sync-alt"></i> Refresh
|
|
||||||
</button>
|
|
||||||
<div class="theme-toggle">
|
<div class="theme-toggle">
|
||||||
<span>Theme</span>
|
<span>Theme</span>
|
||||||
<input id="themeSwitch" type="checkbox" />
|
<input id="themeSwitch" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
||||||
|
<button id="ajaxUpdateBtn" class="ajax-update-btn" title="AJAX Auto-update">update</button>
|
||||||
|
<button class="btn btn-small log-refresh-btn" title="Обновить логи и счетчики">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2226,6 +2280,7 @@ const els = {
|
|||||||
|
|
||||||
filter: document.getElementById('filter'),
|
filter: document.getElementById('filter'),
|
||||||
wsstate: document.getElementById('wsstate'),
|
wsstate: document.getElementById('wsstate'),
|
||||||
|
ajaxUpdateBtn: document.getElementById('ajaxUpdateBtn'),
|
||||||
projectBadge: document.getElementById('projectBadge'),
|
projectBadge: document.getElementById('projectBadge'),
|
||||||
|
|
||||||
clearBtn: document.getElementById('clear'),
|
clearBtn: document.getElementById('clear'),
|
||||||
@@ -2288,6 +2343,28 @@ function setWsState(s){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAjaxUpdateState(enabled) {
|
||||||
|
console.log('setAjaxUpdateState: enabled =', enabled, 'els.ajaxUpdateBtn =', !!els.ajaxUpdateBtn);
|
||||||
|
|
||||||
|
if (els.ajaxUpdateBtn) {
|
||||||
|
// Удаляем все классы состояний
|
||||||
|
els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off');
|
||||||
|
|
||||||
|
// Добавляем соответствующий класс
|
||||||
|
if (enabled) {
|
||||||
|
els.ajaxUpdateBtn.classList.add('ajax-on');
|
||||||
|
els.ajaxUpdateBtn.textContent = 'update';
|
||||||
|
console.log('setAjaxUpdateState: Устанавливаем зеленый цвет (ajax-on)');
|
||||||
|
} else {
|
||||||
|
els.ajaxUpdateBtn.classList.add('ajax-off');
|
||||||
|
els.ajaxUpdateBtn.textContent = 'update';
|
||||||
|
console.log('setAjaxUpdateState: Устанавливаем красный цвет (ajax-off)');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('setAjaxUpdateState: Кнопка ajaxUpdateBtn не найдена!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для обновления всех логов при изменении фильтров
|
// Функция для обновления всех логов при изменении фильтров
|
||||||
function refreshAllLogs() {
|
function refreshAllLogs() {
|
||||||
// Обновляем обычный просмотр
|
// Обновляем обычный просмотр
|
||||||
@@ -2784,11 +2861,12 @@ function buildTabs(){
|
|||||||
</div>
|
</div>
|
||||||
<div class="container-service">
|
<div class="container-service">
|
||||||
${escapeHtml(svc.service || svc.name)}
|
${escapeHtml(svc.service || svc.name)}
|
||||||
${svc.project ? ` • ${escapeHtml(svc.project)}` : ''}
|
• ${escapeHtml(svc.project || 'standalone')}
|
||||||
</div>
|
</div>
|
||||||
<div class="container-status">
|
<div class="container-status">
|
||||||
<span class="status-indicator ${statusClass}"></span>
|
<span class="status-indicator ${statusClass}"></span>
|
||||||
${escapeHtml(svc.status)}
|
${escapeHtml(svc.status)}
|
||||||
|
${svc.status === 'running' && svc.host_port ? ` on ${svc.host_port} port` : ''}
|
||||||
${svc.url ? `<a href="${svc.url}" target="_blank" class="container-link" title="Открыть сайт"><i class="fas fa-external-link-alt"></i></a>` : ''}
|
${svc.url ? `<a href="${svc.url}" target="_blank" class="container-link" title="Открыть сайт"><i class="fas fa-external-link-alt"></i></a>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="container-select">
|
<div class="container-select">
|
||||||
@@ -5577,6 +5655,17 @@ els.refreshBtn.onclick = async () => {
|
|||||||
btn.addEventListener('click', refreshLogsAndCounters);
|
btn.addEventListener('click', refreshLogsAndCounters);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обработчик для кнопки update (AJAX autoupdate toggle)
|
||||||
|
if (els.ajaxUpdateBtn) {
|
||||||
|
console.log('Инициализация обработчика клика для кнопки update');
|
||||||
|
els.ajaxUpdateBtn.addEventListener('click', () => {
|
||||||
|
console.log('Клик по кнопке update - вызываем toggleAjaxLogUpdate()');
|
||||||
|
toggleAjaxLogUpdate();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Кнопка ajaxUpdateBtn не найдена при инициализации обработчика!');
|
||||||
|
}
|
||||||
|
|
||||||
// Обработчики для счетчиков
|
// Обработчики для счетчиков
|
||||||
function addCounterClickHandlers() {
|
function addCounterClickHandlers() {
|
||||||
// Назначаем обработчики на все дубликаты кнопок (и в log-header, и в header-compact-controls)
|
// Назначаем обработчики на все дубликаты кнопок (и в log-header, и в header-compact-controls)
|
||||||
@@ -6046,6 +6135,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const isHidden = sidebarControls.classList.contains('hidden');
|
const isHidden = sidebarControls.classList.contains('hidden');
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
|
// Если сайдбар свернут, сначала разворачиваем его
|
||||||
|
if (els.sidebar.classList.contains('collapsed')) {
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
// Показываем настройки
|
// Показываем настройки
|
||||||
sidebarControls.classList.remove('hidden');
|
sidebarControls.classList.remove('hidden');
|
||||||
els.optionsBtn.classList.remove('active');
|
els.optionsBtn.classList.remove('active');
|
||||||
@@ -6708,7 +6801,7 @@ window.addEventListener('keydown', async (e)=>{
|
|||||||
|
|
||||||
// Глобальные переменные для AJAX обновления
|
// Глобальные переменные для AJAX обновления
|
||||||
let ajaxUpdateInterval = null;
|
let ajaxUpdateInterval = null;
|
||||||
let ajaxUpdateEnabled = false;
|
let ajaxUpdateEnabled = true; // По умолчанию включен
|
||||||
let ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию (будет переопределено из env)
|
let ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию (будет переопределено из env)
|
||||||
|
|
||||||
// Состояние для каждого контейнера (для multi-view)
|
// Состояние для каждого контейнера (для multi-view)
|
||||||
@@ -6741,6 +6834,9 @@ window.addEventListener('keydown', async (e)=>{
|
|||||||
|
|
||||||
// Обновляем UI
|
// Обновляем UI
|
||||||
updateAjaxUpdateCheckbox();
|
updateAjaxUpdateCheckbox();
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh и состояние кнопки update
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6757,17 +6853,29 @@ window.addEventListener('keydown', async (e)=>{
|
|||||||
|
|
||||||
// Обновляем UI
|
// Обновляем UI
|
||||||
updateAjaxUpdateCheckbox();
|
updateAjaxUpdateCheckbox();
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh и состояние кнопки update
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Переключить состояние AJAX обновления
|
* Переключить состояние AJAX обновления
|
||||||
*/
|
*/
|
||||||
function toggleAjaxLogUpdate() {
|
function toggleAjaxLogUpdate() {
|
||||||
|
console.log('toggleAjaxLogUpdate: Текущее состояние ajaxUpdateEnabled =', ajaxUpdateEnabled);
|
||||||
|
|
||||||
if (ajaxUpdateEnabled) {
|
if (ajaxUpdateEnabled) {
|
||||||
|
console.log('toggleAjaxLogUpdate: Отключаем AJAX update');
|
||||||
disableAjaxLogUpdate();
|
disableAjaxLogUpdate();
|
||||||
} else {
|
} else {
|
||||||
|
console.log('toggleAjaxLogUpdate: Включаем AJAX update');
|
||||||
enableAjaxLogUpdate(ajaxUpdateIntervalMs);
|
enableAjaxLogUpdate(ajaxUpdateIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('toggleAjaxLogUpdate: Новое состояние ajaxUpdateEnabled =', ajaxUpdateEnabled);
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh и состояние кнопки update при переключении
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6979,6 +7087,35 @@ window.addEventListener('keydown', async (e)=>{
|
|||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.checked = ajaxUpdateEnabled;
|
checkbox.checked = ajaxUpdateEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh в зависимости от состояния ajax autoupdate
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновить видимость кнопки refresh в header и состояние кнопки update
|
||||||
|
*/
|
||||||
|
function updateRefreshButtonVisibility() {
|
||||||
|
console.log('updateRefreshButtonVisibility: ajaxUpdateEnabled =', ajaxUpdateEnabled);
|
||||||
|
|
||||||
|
const refreshButtons = document.querySelectorAll('.log-refresh-btn');
|
||||||
|
console.log('updateRefreshButtonVisibility: Найдено кнопок refresh =', refreshButtons.length);
|
||||||
|
|
||||||
|
refreshButtons.forEach(btn => {
|
||||||
|
if (ajaxUpdateEnabled) {
|
||||||
|
// Если ajax autoupdate включен, скрываем кнопку refresh
|
||||||
|
btn.style.display = 'none';
|
||||||
|
console.log('updateRefreshButtonVisibility: Скрываем кнопку refresh');
|
||||||
|
} else {
|
||||||
|
// Если ajax autoupdate выключен, показываем кнопку refresh
|
||||||
|
btn.style.display = 'inline-flex';
|
||||||
|
console.log('updateRefreshButtonVisibility: Показываем кнопку refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем состояние кнопки update
|
||||||
|
console.log('updateRefreshButtonVisibility: Обновляем состояние кнопки update');
|
||||||
|
setAjaxUpdateState(ajaxUpdateEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7001,12 +7138,18 @@ window.addEventListener('keydown', async (e)=>{
|
|||||||
} else {
|
} else {
|
||||||
disableAjaxLogUpdate();
|
disableAjaxLogUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh и состояние кнопки update при изменении состояния чекбокса
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Устанавливаем начальное состояние (включен по умолчанию)
|
// Устанавливаем начальное состояние (включен по умолчанию)
|
||||||
checkbox.checked = true;
|
checkbox.checked = true;
|
||||||
ajaxUpdateEnabled = true;
|
ajaxUpdateEnabled = true;
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh и состояние кнопки update при инициализации
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
|
|
||||||
console.log('AJAX Update Checkbox initialized');
|
console.log('AJAX Update Checkbox initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7064,6 +7207,9 @@ window.addEventListener('keydown', async (e)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log('AJAX обновление логов инициализировано');
|
console.log('AJAX обновление логов инициализировано');
|
||||||
|
|
||||||
|
// Обновляем видимость кнопки refresh и состояние кнопки update после инициализации
|
||||||
|
updateRefreshButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем инициализацию AJAX обновления
|
// Запускаем инициализацию AJAX обновления
|
||||||
|
|||||||
Reference in New Issue
Block a user