Compare commits

...

10 Commits

Author SHA1 Message Date
Влад Зверев
c72df3fd00 Small duty fix. now active triggers sorted order from past to now 2024-09-19 16:37:47 +05:00
Влад Зверев
889a408c57 Small cleanup code 2024-09-19 15:36:19 +05:00
Влад Зверев
a1b961ae20 Small cleanup code 2024-09-19 15:21:52 +05:00
Влад Зверев
4ef050e3c8 Update logging system 2024-09-19 02:55:34 +05:00
Влад Зверев
54784a41da Update Dockerfile 2024-09-18 20:46:54 +05:00
Влад Зверев
21834d7d71 Massive rework of menu,
Add endpoint telezab/users
Add endpoint telezab/users/add
Add endpoint telezab/users/del
Add endpoint telezab/users/get
Add endpoint telezab/regions
Add endpoint telezab/regions/add
Add endpoint telezab/regions/del
Add endpoint telezab/regions/get
Rework Active Triggers button now don't need subscription
Rework Help button
Add option to change what Notification type you want reciving All or Disaster Only
Rework Settings button removed some misc buttons
Rework Registration mechanism now using POST JSON users/add
Rework formating of Zabbix Triggers for Active triggers and Notification from Zabbix
2024-09-18 20:12:29 +05:00
Влад Зверев
dd66cb5712 rework Active triggers function
rework settings menu button
add function to choose what severity level you want to receive
2024-09-09 17:05:46 +05:00
Влад Зверев
0d2003b335 Cleanup requirements.txt from unused packages 2024-08-02 18:52:00 +05:00
Влад Зверев
f81b268d34 Cleanup requirements.txt from unused packages 2024-08-02 13:06:42 +05:00
Влад Зверев
391f35e4be Cleanup requirements.txt from unused packages 2024-08-02 12:04:25 +05:00
61 changed files with 73139 additions and 774 deletions

View File

@ -5,3 +5,7 @@
/.gitignore
/.git
/logs/
/telezab.db
/db/
/db/telezab.db

6
.gitignore vendored
View File

@ -5,3 +5,9 @@
/logs
/__pycache__
/venv
/app.log
/logs/
/logs/app.log
/logs/error.log
/db/
/db/telezab.db

View File

@ -1,12 +1,17 @@
FROM python:3.11.9-slim
FROM python:3.12.3-slim
LABEL authors="UdoChudo"
# Установим необходимые пакеты
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
gcc \
tzdata \
sqlite3 \
curl \
telnet \
&& rm -rf /var/lib/apt/lists/*
# Установим рабочую директорию
WORKDIR /app
@ -18,6 +23,9 @@ RUN pip install --no-cache-dir -r requirements.txt
# Откроем порт для нашего приложения
EXPOSE 5000
ENV FLASK_APP=telezab.py
ENV TZ=Europe/Moscow
ENV FLASK_APP telezab.py
ENV PYTHONUNBUFFERED 1
# Запуск Gunicorn
CMD ["python3", "telezab.py"]

201
log_manager.py Normal file
View File

@ -0,0 +1,201 @@
import logging
from logging.config import dictConfig
from logging.handlers import TimedRotatingFileHandler
import os
import zipfile
from datetime import datetime, timedelta
class UTF8StreamHandler(logging.StreamHandler):
def __init__(self, stream=None):
super().__init__(stream)
self.setStream(stream)
def setStream(self, stream):
super().setStream(stream)
if hasattr(stream, 'reconfigure'):
stream.reconfigure(encoding='utf-8')
class FilterByMessage(logging.Filter):
def filter(self, record):
# Фильтруем сообщения, содержащие 'Received 1 new updates'
return 'Received ' not in record.getMessage()
class LogManager:
def __init__(self, log_dir='logs', retention_days=30):
self.log_dir = log_dir
self.retention_days = retention_days
self.log_files = {
'flask': os.path.join(self.log_dir, 'flask.log'),
'flask_error': os.path.join(self.log_dir, 'flask_error.log'),
'app': os.path.join(self.log_dir, 'app.log'),
'app_error': os.path.join(self.log_dir, 'app_error.log'),
'debug': os.path.join(self.log_dir, 'debug.log'),
}
# Ensure the log directory exists
if not os.path.exists(self.log_dir):
os.makedirs(self.log_dir)
# Setup logging configuration
self.setup_logging()
def setup_logging(self):
dictConfig({
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default': {
'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s',
},
'error': {
'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s',
},
'werkzeug': {
'format': '[%(asctime)s] %(levelname)s %(message)s'
},
'debug': {
'format': '[%(asctime)s] %(levelname)s %(module)s [%(funcName)s:%(lineno)d]: %(message)s'
}
},
'handlers': {
'telebot_console': {
'class': 'log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout',
'formatter': 'default',
},
'flask_console': {
'class': 'log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout',
'formatter': 'werkzeug',
},
'flask_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['flask'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'werkzeug',
'encoding': 'utf-8',
},
'flask_error_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['flask_error'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'werkzeug',
'encoding': 'utf-8',
'level': 'ERROR',
},
'app_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['app'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'default',
'encoding': 'utf-8',
},
'app_error_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['app_error'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'error',
'encoding': 'utf-8',
'level': 'ERROR',
},
'debug_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['debug'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'debug',
'encoding': 'utf-8',
'level': 'DEBUG',
},
},
'loggers': {
'flask': {
'level': 'INFO',
'handlers': ['flask_file', 'flask_error_file', 'flask_console'],
'propagate': False,
},
'telebot': {
'level': 'INFO',
'handlers': ['app_file', 'app_error_file', 'telebot_console'],
'propagate': False,
},
'werkzeug': {
'level': 'INFO',
'handlers': ['flask_file', 'flask_error_file', 'flask_console'],
'propagate': False,
},
'debug': {
'level': 'DEBUG',
'handlers': ['debug_file'],
'propagate': False,
},
}
# 'root': {
# 'level': 'DEBUG',
# 'handlers': ['flask_console', 'telebot_console'],
# }
})
def archive_old_logs(self):
"""Archives old log files and removes logs older than retention_days."""
# Get yesterday's date
yesterday_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
for log_name, log_file in self.log_files.items():
if os.path.exists(log_file):
archive_name = f"{log_name}_{yesterday_date}.zip"
archive_path = os.path.join(self.log_dir, archive_name)
# Archive the log file
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.write(log_file, arcname=os.path.basename(log_file))
# Remove the old log file after archiving
os.remove(log_file)
# Clean up old archives
self.cleanup_old_archives()
def configure_werkzeug_logging(self):
"""Отключаем встроенный логгер Werkzeug и задаём собственные настройки логирования."""
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.handlers = [] # Удаляем существующие обработчики
# Добавляем кастомный обработчик для форматирования логов
handler = TimedRotatingFileHandler(self.log_files['flask'], when='midnight', backupCount=self.retention_days, encoding='utf-8')
handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s %(message)s'))
werkzeug_logger.addHandler(handler)
# Отключаем дублирование логов
werkzeug_logger.propagate = False
def cleanup_old_archives(self):
"""Deletes archived logs older than retention_days."""
now = datetime.now()
cutoff = now - timedelta(days=self.retention_days)
for file in os.listdir(self.log_dir):
if file.endswith('.zip'):
file_path = os.path.join(self.log_dir, file)
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_time < cutoff:
os.remove(file_path)
def schedule_log_rotation(self):
"""Schedules daily log rotation and archiving."""
from threading import Timer
now = datetime.now()
next_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
delay = (next_midnight - now).total_seconds()
Timer(delay, self.rotate_and_archive_logs).start()
def rotate_and_archive_logs(self):
"""Rotates and archives logs."""
self.archive_old_logs()
self.schedule_log_rotation() # Schedule the next rotation

107
region_api.py Normal file
View File

@ -0,0 +1,107 @@
import sqlite3
import logging
from threading import Lock
db_lock = Lock()
# Инициализируем логгер
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования
class RegionAPI:
def __init__(self, db_path):
self.db_path = db_path
def add_region(self, region_id: int, region_name: str):
logger.info(f"Запрос на добавление региона: id={region_id}, name={region_name}")
# Проверка валидности region_id
if not str(region_id).isdigit():
logger.error(f"region_id {region_id} не является числом.")
return {"status": "failure", "message": "Region_id must be digit only"}
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
# Добавляем новый регион
cursor.execute('INSERT INTO regions (region_id, region_name, active) VALUES (?, ?, 1)',
(region_id, region_name))
conn.commit()
logger.info(f"Регион с id={region_id} успешно добавлен.")
return {"status": "success", "message": "Region added successfully"}
else:
logger.warning(f"Регион с id={region_id} уже существует.")
return {"status": "error", "message": "Region already exists"}
def remove_region(self, region_id):
logger.info(f"Запрос на удаление региона: id={region_id}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,))
conn.commit()
logger.info(f"Регион с id={region_id} успешно удалён.")
return {"status": "success", "message": "Region removed successfully"}
def get_regions(self):
logger.info("Запрос на получение списка регионов.")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug("Извлечение данных из таблицы regions.")
cursor.execute('SELECT region_id, region_name, active FROM regions')
regions = cursor.fetchall()
logger.info(f"Получено {len(regions)} регионов.")
return [{"region_id": r[0], "region_name": r[1], "regions_active": r[2]} for r in regions]
def change_region_status(self, region_id, active):
logger.info(f"Запрос на изменение статуса региона: id={region_id}, статус={active}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id))
conn.commit()
logger.info(f"Статус региона с id={region_id} успешно изменён.")
return {"status": "success", "message": "Region status updated successfully"}
def update_region_status(self, region_id, active):
logger.info(f"Запрос на обновление статуса региона: id={region_id}, активность={active}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем существование региона
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,))
result = cursor.fetchone()
if not result:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Регион не найден"}
# Обновляем статус активности региона
cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id))
conn.commit()
action = "Активирован" if active else "Отключён"
logger.info(f"Регион с id={region_id} {action}.")
return {"status": "success", "message": f"Регион {region_id} {action}"}

Binary file not shown.

5051
static/css/bootstrap-grid.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/css/bootstrap-grid.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5050
static/css/bootstrap-grid.rtl.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/css/bootstrap-grid.rtl.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

485
static/css/bootstrap-reboot.css vendored Normal file
View File

@ -0,0 +1,485 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

8
static/css/bootstrap-reboot.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

482
static/css/bootstrap-reboot.rtl.css vendored Normal file
View File

@ -0,0 +1,482 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4866
static/css/bootstrap-utilities.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4857
static/css/bootstrap-utilities.rtl.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11266
static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11242
static/css/bootstrap.rtl.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/css/bootstrap.rtl.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

23
static/css/regions.css Normal file
View File

@ -0,0 +1,23 @@
/* Увеличение ширины колонки с регионами */
#region-list {
max-height: 600px; /* Устанавливаем максимальную высоту для вертикальной прокрутки */
overflow-y: auto;
}
#user-list {
max-height: 400px; /* Устанавливаем максимальную высоту для вертикальной прокрутки */
overflow-y: auto;
}
/* Стилизация заголовков и кнопок */
h3 {
margin-bottom: 20px;
}
#user-info {
margin-bottom: 20px;
}
.btn {
margin: 0 5px; /* Добавляем немного пространства между кнопками */
}

102
static/css/styles.css Normal file
View File

@ -0,0 +1,102 @@
/* Обнуляем отступы для более гибкого макета */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Основные контейнеры */
html, body {
height: 100%;
width: 100%;
}
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
}
/* Основной контейнер, занимает весь экран */
.container {
display: flex;
height: 100%;
width: 100%;
padding: 20px;
box-sizing: border-box;
}
/* Контейнер для списка пользователей */
.user-list-container {
width: 30%; /* Список пользователей занимает 30% ширины */
display: flex;
flex-direction: column;
margin-right: 20px;
}
/* Прокручивающийся список пользователей */
.user-list {
flex: 1;
max-height: 100%;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
}
.user-item {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
cursor: pointer;
}
.user-item.selected {
background-color: #f0f0f0;
}
/* Блок управления */
.controls {
display: flex;
flex-direction: column;
width: 70%; /* Блок с информацией и кнопкой занимает 70% ширины */
}
#show-events {
margin-bottom: 20px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
#user-info {
margin-top: 10px;
}
#events {
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
}
#events ul {
list-style: none;
padding: 0;
}
#events ul li {
margin-bottom: 5px;
}
/* Контейнер для строки поиска */
.search-container {
margin-bottom: 10px;
}
.search-container input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}

25
static/css/users.css Normal file
View File

@ -0,0 +1,25 @@
/* Добавим выравнивание и отступы для кнопок управления */
.table-hover tbody tr td {
vertical-align: middle;
}
.table-hover tbody tr td .btn-manage {
margin-left: 20px; /* Отступ кнопки от названия региона */
}
th, td {
text-align: left; /* Выровнять содержимое слева */
}
.table-hover tbody tr {
height: 50px; /* Сделать строки таблицы выше для лучшего визуального эффекта */
}
.modal-footer .btn {
margin-left: 10px;
}
/* Стили для пагинации */
.d-flex .btn {
margin: 0 5px;
}

6812
static/js/bootstrap.bundle.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4999
static/js/bootstrap.esm.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/js/bootstrap.esm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5046
static/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10881
static/js/jquery-3.6.0.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
static/js/jquery-3.6.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

133
static/js/regions.js Normal file
View File

@ -0,0 +1,133 @@
$(document).ready(function() {
const regionList = $('#region-list');
const userInfo = $('#user-info');
const userList = $('#user-list');
const curentRegion = $('#curent-region')
const pagination = $('#pagination');
let regions = [];
let users = [];
let selectedRegionName = null;
let selectedRegionId = null;
const regionsPerPage = 10;
let currentPage = 1;
// Загрузка регионов
function loadRegions() {
$.getJSON('/telezab/regions/get', function(data) {
regions = data;
regions.sort((a, b) => a.region_id - b.region_id); // Сортировка по региону
renderRegions();
renderPagination();
}).fail(function() {
console.error("Не удалось получить регионы.");
});
}
// Загрузка пользователей
function loadUsers() {
$.getJSON('/telezab/users/get', function(data) {
users = data;
}).fail(function() {
console.error("Не удалось получить пользователей.");
});
}
// Отрисовка списка регионов
function renderRegions() {
regionList.empty();
const start = (currentPage - 1) * regionsPerPage;
const end = start + regionsPerPage;
const paginatedRegions = regions.slice(start, end);
paginatedRegions.forEach(region => {
const listItem = $('<a>', {
href: '#',
class: 'list-group-item list-group-item-action',
text: `${region.region_name} (${region.region_id})`,
'data-region-id': region.region_id,
'data-region-name': region.region_name
}).on('click', function() {
selectedRegionId = $(this).data('region-id');
selectedRegionName = $(this).data('region-name');
showRegionDetails();
});
regionList.append(listItem);
});
}
// Отрисовка кнопок пагинации
function renderPagination() {
pagination.empty();
const totalPages = Math.ceil(regions.length / regionsPerPage);
for (let i = 1; i <= totalPages; i++) {
const pageItem = $('<li>', {
class: `page-item ${i === currentPage ? 'active' : ''}`
}).append($('<a>', {
class: 'page-link',
href: '#',
text: i
}).on('click', function(e) {
e.preventDefault();
currentPage = i;
renderRegions();
renderPagination();
}));
pagination.append(pageItem);
}
}
// Показать детали региона
function showRegionDetails() {
userInfo.empty();
userList.empty();
curentRegion.empty(); // Очистить текущий элемент
// Найти текущий регион по selectedRegionId
const selectedRegion = regions.find(region => region.region_name === selectedRegionName);
// Проверить, что регион найден
if (selectedRegion) {
// Создать и добавить заголовок h3 с именем региона
var newHeading = $('<h3>').text(selectedRegion.region_name);
$('#curent-region').append(newHeading);
} else {
$('#curent-region').text('Регион не найден.');
}
// Фильтровать пользователей, подписанных на этот регион
const subscribedUsers = users.filter(user =>
user.subscriptions.split(', ').includes(selectedRegionId.toString())
);
// Показать список пользователей или сообщение, если подписчиков нет
if (subscribedUsers.length > 0) {
subscribedUsers.forEach(user => {
userList.append(`<li>${user.email}</li>`);
});
} else {
userInfo.text('Нет подписчиков на этот регион.');
}
}
// Инициализация
loadRegions();
loadUsers();
// Обработчики кнопок в модальном окне
$('#delete-region').on('click', function() {
// Функция удаления региона
console.log('Удалить регион', selectedRegionId);
});
$('#disable-region').on('click', function() {
// Функция отключения региона
console.log('Отключить регион', selectedRegionId);
});
$('#activate-region').on('click', function() {
// Функция активации региона
console.log('Активировать регион', selectedRegionId);
});
});

55
static/js/users.js Normal file
View File

@ -0,0 +1,55 @@
$(document).ready(function() {
// Получаем список сотрудников
$.getJSON('/telezab/users/get', function(data) {
var userList = $('#user-list');
userList.empty();
data.forEach(function(user) {
var email = user.email;
var name = email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase());
var listItem = $('<li class="list-group-item"></li>').text(name).data('user', user);
userList.append(listItem);
});
});
// Обработчик кликов по пользователям
$('#user-list').on('click', '.list-group-item', function() {
// Удаляем активный класс у всех элементов списка
$('.list-group-item').removeClass('active');
// Добавляем активный класс к выбранному элементу
$(this).addClass('active');
var user = $(this).data('user');
$('#user-info').removeClass('d-none');
$('#user-name').text(user.email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase()));
// Отображаем регионы в одну строку
var regions = $('#user-regions');
regions.empty();
if (user.subscriptions) {
var subscriptions = user.subscriptions.split(',').map(function(sub) { return sub.trim(); });
if (subscriptions.length > 0) {
regions.text(subscriptions.join(', '));
} else {
regions.text('Нет подписок');
}
} else {
regions.text('Нет подписок');
}
// Отображаем действия
var events = $('#user-events');
events.empty();
if (user.events && user.events.length > 0) {
user.events.forEach(function(event) {
var eventText = event.type;
if (event.region) {
eventText += ' (Регион: ' + event.region + ')';
}
events.append('<div><strong>' + event.date + '</strong> - ' + eventText + '</div>');
});
} else {
events.append('<div>Нет действий</div>');
}
});
});

1983
telezab.py

File diff suppressed because it is too large Load Diff

53
templates/regions.html Normal file
View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Region Management</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/regions.css') }}">
</head>
<body>
<div class="container mt-4">
<div class="row">
<!-- Левый контейнер со списком регионов -->
<div class="col-md-6">
<h3>Список Регионов</h3>
<div class="list-group" id="region-list">
<!-- Список регионов будет добавляться сюда -->
</div>
<!-- Пагинация -->
<nav aria-label="Page navigation">
<ul class="pagination" id="pagination">
<!-- Кнопки пагинации будут добавляться сюда -->
</ul>
</nav>
</div>
<!-- Правый контейнер с информацией о пользователях -->
<div class="col-md-6">
<h3>Пользователи</h3>
<div id="curent-region">
</div>
<div id="user-info">
<!-- Информация о пользователях будет добавляться сюда -->
</div>
<ul id="user-list">
<!-- Список пользователей будет добавляться сюда -->
</ul>
<!-- Кнопки управления регионами -->
<div class="d-flex justify-content-end mt-3">
<button type="button" class="btn btn-danger me-2" id="delete-region">Удалить регион</button>
<button type="button" class="btn btn-warning me-2" id="disable-region">Отключить регион</button>
<button type="button" class="btn btn-success" id="activate-region">Активировать регион</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/regions.js') }}"></script>
</body>
</html>

37
templates/users.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сотрудники</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-4">
<ul id="user-list" class="list-group">
<!-- Список сотрудников будет вставлен сюда -->
</ul>
</div>
<div class="col-md-8">
<div id="user-info" class="d-none">
<h3 id="user-name"></h3>
<h5>Подписки на регионы</h5>
<ul id="user-regions" class="list-group">
<!-- Подписки на регионы будут вставлены сюда -->
</ul>
<h5>Действия</h5>
<div id="user-events" class="overflow-auto" style="max-height: 300px;">
<!-- Действия будут вставлены сюда -->
</div>
</div>
</div>
</div>
</div>
</body>
</html>

16
user_state_manager.py Normal file
View File

@ -0,0 +1,16 @@
class UserStateManager:
def __init__(self):
# Словарь для хранения состояния каждого пользователя
self.user_states = {}
def set_state(self, chat_id, state):
"""Устанавливает состояние для пользователя."""
self.user_states[chat_id] = state
def get_state(self, chat_id):
"""Получает текущее состояние пользователя."""
return self.user_states.get(chat_id, "MAIN_MENU")
def reset_state(self, chat_id):
"""Сбрасывает состояние пользователя в главное меню."""
self.user_states[chat_id] = "MAIN_MENU"