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
This commit is contained in:
Влад Зверев 2024-09-18 20:12:29 +05:00
parent dd66cb5712
commit 21834d7d71
60 changed files with 72775 additions and 845 deletions

View File

@ -6,3 +6,6 @@
/.git
/logs/
/telezab.db
/db/
/db/telezab.db

View File

@ -1,4 +1,4 @@
FROM python:3.11.9-slim
FROM python:3.12.3-slim
LABEL authors="UdoChudo"
# Установим необходимые пакеты
RUN apt-get update && apt-get install -y \

157
log_manager.py Normal file
View File

@ -0,0 +1,157 @@
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'),
}
# 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',
},
},
'handlers': {
'console': {
'class': 'log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout',
'formatter': 'default',
},
'flask_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['flask'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'default',
'encoding': 'utf-8',
},
'flask_error_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['flask_error'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'error',
'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',
},
},
'loggers': {
'flask': {
'level': 'INFO',
'handlers': ['flask_file', 'flask_error_file', 'console'],
'propagate': False,
},
'telebot': {
'level': 'INFO',
'handlers': ['app_file', 'app_error_file', 'console'],
'propagate': False,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['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 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

73
region_api.py Normal file
View File

@ -0,0 +1,73 @@
# region_api.py
import sqlite3
from threading import Lock
db_lock = Lock()
class RegionAPI:
def __init__(self, db_path):
self.db_path = db_path
def add_region(self, region_id: int, region_name: str):
if not region_id.isdigit() == True:
return {"status": "failure", "message": "Region_id must be digit only"}
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
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()
return {"status": "success", "message": "Region added successfully"}
else:
return {"status": "error", "message": "Region already exists"}
def remove_region(self, region_id):
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,))
conn.commit()
return {"status": "success", "message": "Region removed successfully"}
def get_regions(self):
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('SELECT region_id, region_name, active FROM regions')
regions = cursor.fetchall()
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):
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id))
conn.commit()
return {"status": "success", "message": "Region status updated successfully"}
def update_region_status(self, region_id, active):
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем существование региона
cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,))
result = cursor.fetchone()
if not result:
return {"status": "error", "message": "Регион не найден"}
# Обновляем статус активности региона
cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id))
conn.commit()
action = "Активирован" if active else "Отключён"
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>');
}
});
});

1801
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"