diff --git a/app.db b/app.db index f30c372..da93a9a 100644 Binary files a/app.db and b/app.db differ diff --git a/app.py b/app.py index 4fbe215..0e2436f 100644 --- a/app.py +++ b/app.py @@ -19,6 +19,7 @@ os.makedirs("logs", exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", handlers=[ logging.FileHandler(Config.LOG_FILE, encoding="utf-8"), logging.StreamHandler(), @@ -28,6 +29,14 @@ logging.basicConfig( logger = logging.getLogger(__name__) +def getLogs(): + from collections import deque + + with open(Config.LOG_FILE, "r", encoding="utf-8") as f: + last_lines = deque(f, maxlen=500) + return "".join(reversed(last_lines)) + + @app.before_request def init(): db.create_all() @@ -35,9 +44,79 @@ def init(): logger.info("Приложение запущено") -@app.route("/") +@app.route("/", methods=["GET", "POST"]) def index(): - return render_template("index.html") + match request.method: + case "GET": + exitData = {} + medodsApi = MedodsAPI.query.first() + if medodsApi: + exitData["medodsApi"] = { + "updated_at": medodsApi.updated_at.strftime("%Y-%m-%d %H:%M:%S") + } + if medodsApi.url: + exitData["medodsApi"]["url"] = True + else: + exitData["medodsApi"]["url"] = False + if medodsApi.identity and medodsApi.secretKey: + exitData["medodsApi"]["apiKey"] = True + else: + exitData["medodsApi"]["apiKey"] = False + apiEndpointsCount = ApiEndpoint.query.count() + exitData["medodsApi"]["apiEndpointsCount"] = apiEndpointsCount + vkApi = VkAPI.query.first() + if vkApi: + exitData["vkApi"] = { + "updated_at": vkApi.updated_at.strftime("%Y-%m-%d %H:%M:%S") + } + if vkApi.group_id: + exitData["vkApi"]["group_id"] = True + else: + exitData["vkApi"]["group_id"] = False + if vkApi.access_token: + exitData["vkApi"]["access_token"] = True + else: + exitData["vkApi"]["access_token"] = False + if vkApi.base_photo_url: + exitData["vkApi"]["base_photo_url"] = True + else: + exitData["vkApi"]["base_photo_url"] = False + vkPost = VkPost.query.first() + if vkPost: + exitData["vkPost"] = { + "updated_at": vkPost.updated_at.strftime("%Y-%m-%d %H:%M:%S") + } + if vkPost.static_text: + exitData["vkPost"]["static_text"] = True + else: + exitData["vkPost"]["static_text"] = False + if vkPost.selected_users: + exitData["vkPost"]["selected_users"] = len(vkPost.selected_users) + else: + exitData["vkPost"]["selected_users"] = False + if vkPost.full_name is not None: + exitData["vkPost"]["full_name"] = vkPost.full_name + if vkPost.post_id and vkApi and vkApi.group_id: + exitData["vkPost"][ + "post_link" + ] = f"https://vk.com/wall-{vkApi.group_id}_{vkPost.post_id}" + else: + exitData["vkPost"]["post_link"] = False + if vkPost.publish_at: + exitData["vkPost"]["publish_at"] = vkPost.publish_at.strftime( + "%Y-%m-%d %H:%M:%S" + ) + else: + exitData["vkPost"]["publish_at"] = False + usersMedods = UsersMedods.query.count() + exitData["vkPost"]["usersMedods"] = usersMedods + exitData["vkPost"]["scheduler"] = get_scheduler_status() + + return render_template("index.html", exitData=exitData) + case "POST": + return jsonify(logs=getLogs()) + case _: + return "Method not allowed", 405 @app.route("/medods", methods=["GET"]) diff --git a/scheduler.py b/scheduler.py index e358a48..246c201 100644 --- a/scheduler.py +++ b/scheduler.py @@ -15,6 +15,17 @@ scheduler: BackgroundScheduler | None = None JOB_ID = "vk_publish_job" +def clearLog(): + from collections import deque + from config import Config + + with open(Config.LOG_FILE, "r", encoding="utf-8") as f: + last_lines = deque(f, maxlen=500) + + with open(Config.LOG_FILE, "w", encoding="utf-8") as f: + f.writelines(last_lines) + + def init_scheduler(app): """ Инициализация планировщика с Flask-приложением @@ -26,6 +37,10 @@ def init_scheduler(app): if scheduler is None: scheduler = BackgroundScheduler() + trigger = CronTrigger(hour=0, minute=0) + scheduler.add_job( + clearLog, trigger=trigger, id="clear_log_job", replace_existing=True + ) scheduler.start() diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..9343984 --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,223 @@ +/* Карточки */ +.card { + border-radius: 10px; + border: 1px solid #e9ecef; + transition: all 0.3s ease; + overflow: hidden; +} + +.card:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1); +} + +.card-header { + background-color: rgba(0, 0, 0, 0.02); + border-bottom: 1px solid #e9ecef; + padding: 1rem 1.25rem; +} + +.card-header h5 { + font-weight: 600; +} + +/* Статус индикаторы */ +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} + +.status-online { + background-color: #198754; + box-shadow: 0 0 0 2px rgba(25, 135, 84, 0.2); +} + +.status-offline { + background-color: #6c757d; + box-shadow: 0 0 0 2px rgba(108, 117, 125, 0.2); +} + +/* Быстрые действия */ +.btn-outline-info, +.btn-outline-success, +.btn-outline-warning, +.btn-outline-secondary { + border-width: 2px; + transition: all 0.3s ease; +} + +.btn-outline-info:hover { + background-color: #0dcaf0; + color: white; + border-color: #0dcaf0; +} + +.btn-outline-success:hover { + background-color: #198754; + color: white; + border-color: #198754; +} + +.btn-outline-warning:hover { + background-color: #ffc107; + color: black; + border-color: #ffc107; +} + +.btn-outline-secondary:hover { + background-color: #6c757d; + color: white; + border-color: #6c757d; +} + +/* Логи */ +.logs-container { + max-height: 400px; + overflow-y: auto; + background-color: #f8f9fa; + border-radius: 6px; + padding: 1rem; + font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, 'Courier New', monospace; + font-size: 12px; + line-height: 1.4; +} + +.logs-pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + color: #212529; +} + +.logs-pre .log-info { + color: #0d6efd; +} + +.logs-pre .log-success { + color: #198754; +} + +.logs-pre .log-warning { + color: #ffc107; +} + +.logs-pre .log-error { + color: #dc3545; +} + +/* Адаптивные карточки */ +@media (max-width: 768px) { + .card-header h5 { + font-size: 1rem; + } + + .fw-bold.fs-5 { + font-size: 1.1rem !important; + } + + .logs-container { + font-size: 11px; + } +} + +/* Анимации */ +.fade-in { + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Уведомления */ +.alert-fixed { + position: fixed; + top: 80px; + right: 20px; + z-index: 1050; + min-width: 300px; + max-width: 400px; +} + +/* Карточки с фоном */ +.card.bg-light { + border: none; +} + +/* Информационные блоки */ +.alert { + border-radius: 8px; + border: none; +} + +/* Кнопки действий */ +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +/* Пустые состояния */ +.text-center.py-4 .display-1 { + font-size: 4rem; + color: #dee2e6; + margin-bottom: 1rem; +} + +/* Бейджи */ +.badge { + font-weight: 500; + padding: 0.35em 0.65em; + font-size: 0.85em; +} + +/* Таблицы и сетки */ +.row.g-3 { + margin-top: -0.75rem; +} + +.row.g-3 > [class^="col-"] { + margin-top: 0.75rem; +} + +/* Иконки */ +.bi { + vertical-align: middle; +} + +/* Отступы для контента */ +.card-body { + padding: 1.25rem; +} + +/* Ховер эффекты для карточек быстрых действий */ +.btn:hover .bi { + transform: scale(1.1); + transition: transform 0.3s ease; +} + +/* Скроллбар для логов */ +.logs-container::-webkit-scrollbar { + width: 6px; +} + +.logs-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.logs-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 3px; +} + +.logs-container::-webkit-scrollbar-thumb:hover { + background: #555; +} \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..964a09e --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,140 @@ +// Инициализация при загрузке страницы +document.addEventListener('DOMContentLoaded', function () { + // Устанавливаем время последнего обновления + updateLastUpdateTime(); + + // Добавляем подсветку логов + highlightLogs(); + +}); + +// Обновление времени последнего обновления +function updateLastUpdateTime() { + const now = new Date(); + const timeString = now.toLocaleTimeString('ru-RU', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + const dateString = now.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + + document.getElementById('lastUpdateTime').textContent = `${dateString} ${timeString}`; +} + +// Подсветка логов +function highlightLogs() { + const logsContainer = document.getElementById('logsContainer'); + if (!logsContainer) return; + + const pre = logsContainer.querySelector('pre'); + if (!pre) return; + + let content = pre.innerHTML; + + // Обработка строкового символа + content = content.replace(/\x1B\[[0-9;]*m/g, ''); + + // Подсветка уровней логирования + content = content.replace(/\|\sINFO\s\|/g, '| INFO |'); + content = content.replace(/\|\sWARNING\s\|/g, '| WARNING |'); + content = content.replace(/\|\sERROR\s\|/g, '| ERROR |'); + + // Подсветка дат и времени + content = content.replace(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/g, '$1'); + + // Подсветка IP адресов + content = content.replace(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g, '$1'); + + // Подсветка HTTP методов + content = content.replace(/(GET|POST|PUT|DELETE|PATCH) (\S+)/g, '$1 $2'); + + // Подсветка кодов состояний + content = content.replace( + /"\s(\d{3})\s-/g, + (match, code) => { + let cls = 'text-secondary'; + + if (code >= 200 && code < 300) cls = 'text-success'; + else if (code >= 400 && code < 500) cls = 'text-warning'; + else if (code >= 500) cls = 'text-danger'; + + return `" ${code} -`; + } + ); + + // Подсветка статических файлов + content = content.replace( + /(\/static\/[^\s"]+)/g, + '$1' + ); + + pre.innerHTML = content; +} + +// Переключение видимости логов +function toggleLogs() { + const logsBody = document.getElementById('logsBody'); + const toggleBtn = document.querySelector('.card-header .bi-chevron-down, .card-header .bi-chevron-up'); + const logsDescription = document.getElementById('logs_description'); + + if (logsBody.classList.contains('d-none')) { + logsBody.classList.remove('d-none'); + toggleBtn.classList.remove('bi-chevron-down'); + toggleBtn.classList.add('bi-chevron-up'); + logsDescription.classList.remove('d-none'); + refreshLogs(); + } else { + logsBody.classList.add('d-none'); + logsDescription.classList.add('d-none'); + toggleBtn.classList.remove('bi-chevron-up'); + toggleBtn.classList.add('bi-chevron-down'); + } +} + +// Обновление логов +async function refreshLogs() { + try { + const logsContainer = document.getElementById('logsContainer'); + if (!logsContainer) return; + + const pre = logsContainer.querySelector('pre'); + if (!pre) return; + // Показываем индикатор загрузки + pre.innerHTML = ` +
+
+ Загрузка логов... +
+

Загрузка логов...

+
+ `; + + // Имитация загрузки логов (в реальном приложении здесь будет fetch запрос) + const response = await fetch('/', { + method: 'POST', + }); + + const data = await response.json(); + + pre.innerHTML = data.logs; + highlightLogs(); + + } catch (error) { + console.error('Ошибка при обновлении логов:', error); + } +} + + +function getAlertIcon(type) { + const icons = { + 'success': 'bi-check-circle-fill', + 'warning': 'bi-exclamation-triangle-fill', + 'danger': 'bi-x-circle-fill', + 'info': 'bi-info-circle-fill' + }; + return icons[type] || 'bi-info-circle-fill'; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 45bce00..ab349ae 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,27 +1,430 @@ {% extends "base.html" %} -{% block title %}Главная{% endblock %} +{% block title %}Главная - Панель управления{% endblock %} + +{% block styles %} + +{% endblock %} {% block content %} -

📊 Состояние системы

+ +
+
+

Панель управления

+

Мониторинг состояния системы и статистика

+
+
+ Система +
+
-
-
-
+ +
+ +
+
+
+
Medods API
+ {% if exitData.medodsApi %} + Настроен + {% else %} + Не настроен + {% endif %} +
-
Статус
- Работает + {% if exitData.medodsApi %} +
+
+
+
+
+
+
URL сервера
+
{% if exitData.medodsApi.url %}Настроен{% else %}Не + настроен{% endif %}
+
+
+
+
+
+
+
+
+
API ключ
+
{% if exitData.medodsApi.apiKey %}Настроен{% else %}Не + настроен{% endif %}
+
+
+
+
+
+
+
+
+
+
+
Запросов к серверу
+
{{ exitData.medodsApi.apiEndpointsCount or 0 }} +
+
+
+
Обновлено
+
{{ exitData.medodsApi.updated_at }}
+
+
+
+
+
+
+ {% else %} +
+ +
Medods API не настроен
+

Настройте подключение для работы с API

+ + Перейти к настройкам + +
+ {% endif %} +
+
+
+ + +
+
+
+
VK API
+ {% if exitData.vkApi %} + Настроен + {% else %} + Не настроен + {% endif %} +
+
+ {% if exitData.vkApi %} +
+
+
+
+
+
+
ID группы
+
{% if exitData.vkApi.group_id %}Настроен{% else %}Не + настроен{% endif %}
+
+
+
+
+
+
+
+
+
Токен
+
{% if exitData.vkApi.access_token %}Настроен{% else %}Не + настроен{% endif %}
+
+
+
+
+
+
+
+
+
Базовое фото
+
{% if exitData.vkApi.base_photo_url %}Настроен{% else %}Не + настроен{% endif %}
+
+
+
+
+
+
+
+
+
+
Обновлено
+
{{ exitData.vkApi.updated_at }}
+
+
+
+
+
+ {% else %} +
+ +
VK API не настроен
+

Настройте подключение для работы с VK

+ + Перейти к настройкам + +
+ {% endif %} +
+
+
+ + +
+
+
+
Планировщик
+ {% if exitData.vkPost and exitData.vkPost.scheduler %} + {% if exitData.vkPost.scheduler.scheduler %} + Активен + {% else %} + Неактивен + {% endif %} + {% else %} + Не настроен + {% endif %} +
+
+ {% if exitData.vkPost %} +
+
+
+
+
+
+
Текст поста
+
{% if exitData.vkPost.static_text %}Настроен{% else %}Не + настроен{% endif %}
+
+
+
+
+
+
+
+
+
Автопубликация
+
{% if exitData.vkPost.scheduler.vk_publish_job %}Активна{% else + %}Неактивна{% endif %} +
+
+
+
+
+ + {% if exitData.vkPost.scheduler %} +
+
+
+
+
Обновлено
+
+ {% if exitData.vkPost.updated_at %} + {{ exitData.vkPost.updated_at }} + {% else %} + Не опубликовано + {% endif %} +
+
+
+
Следующий запуск
+
+ {% if exitData.vkPost.scheduler.next_run_time %} + {{ exitData.vkPost.scheduler.next_run_time }} + {% else %} + Не запланирован + {% endif %} +
+
+
+
+
+ {% endif %} + +
+
+
+
+
+
+
Сотрудников в базе
+
{{ exitData.vkPost.usersMedods or 0 }}
+
+
+
Сотрудников выбрано
+
{{ exitData.vkPost.selected_users or 0 }}
+
+
+
+
+
+
+ {% else %} +
+ +
Планировщик не настроен
+

Настройте публикации для запуска планировщика

+ + Перейти к публикациям + +
+ {% endif %}
-

📜 Логи операций

+ +
+ +
+
+
+
+ Переход к настройкам +
+
+ + + +
+
+
+
Последняя публикация
+ {% if exitData.vkPost and exitData.vkPost.post_link %} + Опубликовано + {% else %} + Нет публикаций + {% endif %} +
+
+ {% if exitData.vkPost and exitData.vkPost.post_link %} +
+
+
+ +
+
+
Пост успешно опубликован
+

Ссылка на публикацию в VK

+ + Открыть в VK + +
+
+
+ +
+
+
+
+
Время публикации
+
+ {% if exitData.vkPost.publish_at %} + {{ exitData.vkPost.publish_at }} + {% else %} + Не указано + {% endif %} +
+
+
+
+
+
+
+
Отображение имен
+
+ {% if exitData.vkPost.full_name %} + Полные имена + {% else %} + Короткие имена + {% endif %} +
+
+
+
+
+ {% else %} +
+ +
Нет публикаций
+

Создайте и опубликуйте первый пост

+ + Создать публикацию + +
+ {% endif %} +
+
+ + +
+
+
+
Логи операций
+ (Последние 500 строк. Новые сверху) +
+
+ + +
+
+
+
+

+        
+
+ +
+ + +
+{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/templates/posts.html b/templates/posts.html index a1817ac..49e412f 100644 --- a/templates/posts.html +++ b/templates/posts.html @@ -123,11 +123,11 @@
- +
- Будет добавлен в начало поста перед именами сотрудников + Будет добавлен после свободных слотов сотрудников
@@ -164,6 +164,20 @@
+ +
+
+
+ +
+
+
Временная настройка
+

Посты будут опубликованы в 12:00 ежедневно.
Настройки из расписания будут + проигнорированы!

+
+
+
+
diff --git a/vk_handler.py b/vk_handler.py index 35fb455..5d3b013 100644 --- a/vk_handler.py +++ b/vk_handler.py @@ -33,7 +33,7 @@ def handle_vk_post(): new_post = vk.wall.post( owner_id=-vkApi.group_id, from_group=1, - message=f"{vkPost.dynamic_text}{vkPost.static_text}".strip(), + message=f"{vkPost.dynamic_text}\n{vkPost.static_text}".strip(), attachments=f"photo-{vkApi.group_id}_{vkApi.base_photo_url}", ) logger.info(f"Пост #{new_post.get('post_id')} создан")