style(web): standardize all web templates to clean light minimalist theme
This commit is contained in:
parent
3c31a6a529
commit
6d530c9d97
14 changed files with 1068 additions and 1317 deletions
|
|
@ -1,389 +1,21 @@
|
|||
/* GLASSMORPHISM DARK UI FOR DOMOVOY WEB PANEL */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
/* CLEAN MINIMALIST THEME FOR DOMOVOY WEB PANEL */
|
||||
:root {
|
||||
--bg-main: #06060c;
|
||||
--bg-gradient: radial-gradient(circle at 50% 0%, #150f30 0%, #06060c 80%);
|
||||
--panel-bg: rgba(18, 18, 29, 0.45);
|
||||
--panel-bg-hover: rgba(26, 26, 42, 0.6);
|
||||
--panel-border: rgba(255, 255, 255, 0.08);
|
||||
--panel-border-hover: rgba(255, 255, 255, 0.15);
|
||||
|
||||
--color-primary: #8a2be2;
|
||||
--color-primary-glow: rgba(138, 43, 226, 0.35);
|
||||
--color-success: #00ffcc;
|
||||
--color-success-glow: rgba(0, 255, 204, 0.25);
|
||||
--color-warning: #ffb300;
|
||||
--color-warning-glow: rgba(255, 179, 0, 0.25);
|
||||
--color-danger: #ff2d55;
|
||||
--color-danger-glow: rgba(255, 45, 85, 0.25);
|
||||
--color-info: #00bfff;
|
||||
--color-info-glow: rgba(0, 191, 255, 0.25);
|
||||
|
||||
--color-text-main: #f5f5f7;
|
||||
--color-text-muted: #8e8e93;
|
||||
--color-text-dark: #3a3a3c;
|
||||
|
||||
--font-main: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
--shadow-neon: 0 8px 32px 0 rgba(0, 0, 0, 0.4), 0 0 16px 0 var(--color-primary-glow);
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
--bg-main: #f4f4f4;
|
||||
--bg-panel: #ffffff;
|
||||
--bg-header: #ece9d8;
|
||||
--border-color: #707680;
|
||||
--border-light: #cccccc;
|
||||
--text-main: #111111;
|
||||
--text-muted: #555555;
|
||||
--link-color: #0000cc;
|
||||
--link-hover: #cc0000;
|
||||
--badge-bg: #e8e8e8;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-main) !important;
|
||||
background-image: var(--bg-gradient) !important;
|
||||
background-attachment: fixed !important;
|
||||
color: var(--color-text-main) !important;
|
||||
font-family: var(--font-main) !important;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
line-height: 1.5;
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-main);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Сайдбар */
|
||||
.sidebar {
|
||||
background: rgba(10, 10, 18, 0.3) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
-webkit-backdrop-filter: blur(20px) !important;
|
||||
border-right: 1px solid var(--panel-border) !important;
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: var(--color-text-muted) !important;
|
||||
border-left: 3px solid transparent !important;
|
||||
transition: var(--transition-smooth) !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 500 !important;
|
||||
text-transform: uppercase !important;
|
||||
font-family: var(--font-main) !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
color: var(--color-text-main) !important;
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
border-left-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: var(--color-success) !important;
|
||||
background: rgba(0, 255, 204, 0.05) !important;
|
||||
border-left-color: var(--color-success) !important;
|
||||
text-shadow: 0 0 10px var(--color-success-glow) !important;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i, .sidebar .nav-link bi {
|
||||
margin-right: 0.5rem !important;
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
/* Карточки */
|
||||
.card {
|
||||
background: var(--panel-bg) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
border-radius: 16px !important;
|
||||
transition: var(--transition-smooth) !important;
|
||||
color: var(--color-text-main) !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: var(--panel-bg-hover) !important;
|
||||
border-color: var(--panel-border-hover) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4) !important;
|
||||
transform: translateY(-2px) !important;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: rgba(255, 255, 255, 0.02) !important;
|
||||
border-bottom: 1px solid var(--panel-border) !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
padding: 1rem 1.25rem !important;
|
||||
color: var(--color-text-main) !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
|
||||
/* Переопределение старых рамок и фонов */
|
||||
.border-success, .border-warning, .border-cyan, .border-danger {
|
||||
border-color: var(--panel-border) !important;
|
||||
}
|
||||
|
||||
.card.border-success:hover, .card.border-warning:hover, .card.border-cyan:hover {
|
||||
border-color: var(--panel-border-hover) !important;
|
||||
}
|
||||
|
||||
.card.border-danger {
|
||||
border-color: rgba(255, 45, 85, 0.3) !important;
|
||||
}
|
||||
|
||||
.card.border-danger:hover {
|
||||
border-color: var(--color-danger) !important;
|
||||
box-shadow: 0 8px 30px rgba(255, 45, 85, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Таблицы */
|
||||
.table {
|
||||
color: var(--color-text-main) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > * {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-text-main) !important;
|
||||
border-bottom: 1px solid var(--panel-border) !important;
|
||||
}
|
||||
|
||||
.table > thead > tr > th {
|
||||
color: var(--color-text-muted) !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
font-size: 0.8rem !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
border-bottom: 2px solid var(--panel-border) !important;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover td {
|
||||
background-color: rgba(255, 255, 255, 0.02) !important;
|
||||
color: var(--color-text-main) !important;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn {
|
||||
border-radius: 8px !important;
|
||||
font-weight: 500 !important;
|
||||
transition: var(--transition-smooth) !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
}
|
||||
|
||||
.btn-matrix, .btn-outline-success {
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--color-success) !important;
|
||||
color: var(--color-success) !important;
|
||||
}
|
||||
|
||||
.btn-matrix:hover, .btn-outline-success:hover, .btn-outline-success:active, .btn-outline-success:focus {
|
||||
background-color: var(--color-success) !important;
|
||||
color: #000 !important;
|
||||
border-color: var(--color-success) !important;
|
||||
box-shadow: 0 0 15px var(--color-success-glow) !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
border-color: var(--color-primary) !important;
|
||||
color: var(--color-text-main) !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--color-primary) !important;
|
||||
color: var(--color-text-main) !important;
|
||||
box-shadow: 0 0 15px var(--color-primary-glow) !important;
|
||||
}
|
||||
|
||||
.btn-outline-danger {
|
||||
border-color: var(--color-danger) !important;
|
||||
color: var(--color-danger) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-outline-danger:hover {
|
||||
background-color: var(--color-danger) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 0 15px var(--color-danger-glow) !important;
|
||||
}
|
||||
|
||||
.btn-outline-warning {
|
||||
border-color: var(--color-warning) !important;
|
||||
color: var(--color-warning) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-outline-warning:hover {
|
||||
background-color: var(--color-warning) !important;
|
||||
color: #000 !important;
|
||||
box-shadow: 0 0 15px var(--color-warning-glow) !important;
|
||||
}
|
||||
|
||||
.btn-outline-info {
|
||||
border-color: var(--color-info) !important;
|
||||
color: var(--color-info) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-outline-info:hover {
|
||||
background-color: var(--color-info) !important;
|
||||
color: #000 !important;
|
||||
box-shadow: 0 0 15px var(--color-info-glow) !important;
|
||||
}
|
||||
|
||||
/* Формы */
|
||||
.form-control, .form-select {
|
||||
background-color: rgba(255, 255, 255, 0.03) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
color: var(--color-text-main) !important;
|
||||
border-radius: 8px !important;
|
||||
transition: var(--transition-smooth) !important;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
color: var(--color-text-main) !important;
|
||||
box-shadow: 0 0 8px var(--color-primary-glow) !important;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: var(--color-text-muted) !important;
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
background-color: rgba(10, 10, 15, 0.5) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
}
|
||||
|
||||
/* Ссылки */
|
||||
a {
|
||||
color: var(--color-success);
|
||||
text-decoration: none;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px var(--color-success-glow);
|
||||
}
|
||||
|
||||
/* Баджи */
|
||||
.badge {
|
||||
border-radius: 6px !important;
|
||||
font-weight: 500 !important;
|
||||
padding: 0.4em 0.6em !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: rgba(0, 255, 204, 0.15) !important;
|
||||
color: var(--color-success) !important;
|
||||
border: 1px solid var(--color-success) !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: rgba(255, 45, 85, 0.15) !important;
|
||||
color: var(--color-danger) !important;
|
||||
border: 1px solid var(--color-danger) !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: rgba(255, 179, 0, 0.15) !important;
|
||||
color: var(--color-warning) !important;
|
||||
border: 1px solid var(--color-warning) !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: rgba(0, 191, 255, 0.15) !important;
|
||||
color: var(--color-info) !important;
|
||||
border: 1px solid var(--color-info) !important;
|
||||
}
|
||||
|
||||
/* Утилиты и кастомный контент */
|
||||
hr {
|
||||
border-color: var(--panel-border) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: var(--font-mono) !important;
|
||||
}
|
||||
|
||||
/* Кастомный Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(10, 10, 15, 0.5);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Дополнения для соответствия Swarm Dashboard */
|
||||
.glass-panel {
|
||||
background: var(--panel-bg) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
border-radius: 16px !important;
|
||||
padding: 24px !important;
|
||||
transition: var(--transition-smooth) !important;
|
||||
}
|
||||
|
||||
.glass-panel:hover {
|
||||
background: var(--panel-bg-hover) !important;
|
||||
border-color: var(--panel-border-hover) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
@keyframes neonPulseSuccess {
|
||||
0%, 100% { box-shadow: 0 0 8px var(--color-success-glow); }
|
||||
50% { box-shadow: 0 0 18px rgba(0, 255, 204, 0.6); }
|
||||
}
|
||||
|
||||
@keyframes neonPulseDanger {
|
||||
0%, 100% { box-shadow: 0 0 8px var(--color-danger-glow); }
|
||||
50% { box-shadow: 0 0 18px rgba(255, 45, 85, 0.6); }
|
||||
}
|
||||
|
||||
@keyframes neonPulsePrimary {
|
||||
0%, 100% { box-shadow: 0 0 8px var(--color-primary-glow); }
|
||||
50% { box-shadow: 0 0 18px rgba(138, 43, 226, 0.6); }
|
||||
}
|
||||
|
||||
.glow-success {
|
||||
animation: neonPulseSuccess 3s infinite alternate !important;
|
||||
}
|
||||
|
||||
.glow-danger {
|
||||
animation: neonPulseDanger 3s infinite alternate !important;
|
||||
}
|
||||
|
||||
.glow-primary {
|
||||
animation: neonPulsePrimary 3s infinite alternate !important;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #fff 0%, var(--color-text-muted) 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
.text-gradient-neon {
|
||||
background: linear-gradient(135deg, #00ffcc 0%, #8a2be2 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@
|
|||
border: 1px solid var(--border-light);
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
color: #111111;
|
||||
}
|
||||
pre {
|
||||
padding: 8px;
|
||||
|
|
@ -190,14 +191,80 @@
|
|||
|
||||
.badge-status {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--badge-bg);
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.badge-success { background-color: #e2f0d9 !important; border-color: #70ad47 !important; color: #385723 !important; }
|
||||
.badge-warning { background-color: #fff2cc !important; border-color: #ffc000 !important; color: #806000 !important; }
|
||||
.badge-danger { background-color: #fce4d6 !important; border-color: #ed7d31 !important; color: #c65911 !important; }
|
||||
.badge-info { background-color: #e6f2ff !important; border-color: #66a3ff !important; color: #004080 !important; }
|
||||
|
||||
/* Глобальные переопределения Bootstrap и утилит для светлого стиля */
|
||||
.card, .panel {
|
||||
background-color: var(--bg-panel) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-main) !important;
|
||||
}
|
||||
.card-header, .panel-header {
|
||||
background-color: var(--bg-header) !important;
|
||||
border-bottom: 1px solid var(--border-color) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: bold !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.bg-black, .bg-dark {
|
||||
background-color: #ffffff !important;
|
||||
color: var(--text-main) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
.text-white {
|
||||
color: var(--text-main) !important;
|
||||
}
|
||||
.text-matrix, .text-cyan, .text-success, .text-info {
|
||||
color: var(--text-main) !important;
|
||||
}
|
||||
.btn-matrix {
|
||||
background-color: #e8e8e8 !important;
|
||||
color: #000000 !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: 0 !important;
|
||||
font-family: inherit !important;
|
||||
font-size: 12px !important;
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
.btn-matrix:hover {
|
||||
background-color: #d8d8d8 !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
input, select, textarea, .form-control, .form-select {
|
||||
background-color: #ffffff !important;
|
||||
color: var(--text-main) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: 0 !important;
|
||||
font-family: inherit !important;
|
||||
font-size: 12px !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus, .form-control:focus, .form-select:focus {
|
||||
background-color: #ffffff !important;
|
||||
color: var(--text-main) !important;
|
||||
border-color: #0000cc !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text-main) !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
.shadow, .shadow-sm, .shadow-lg, .shadow-danger {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.badge-success { background-color: #e2f0d9; border-color: #70ad47; color: #385723; }
|
||||
.badge-warning { background-color: #fff2cc; border-color: #ffc000; color: #806000; }
|
||||
.badge-danger { background-color: #fce4d6; border-color: #ed7d31; color: #c65911; }
|
||||
|
||||
{% block extra_style %}{% endblock %}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,24 +7,24 @@
|
|||
{% block extra_style %}
|
||||
<style>
|
||||
#preview { max-width: 400px; margin-top: 1rem; display: none; }
|
||||
#preview img { max-width: 100%; border-radius: 8px; }
|
||||
.smart-options { display: none; background: #f0f7ff; padding: 20px; border-radius: 8px; margin-top: 15px; border: 2px solid #667eea; }
|
||||
#preview img { max-width: 100%; border: 1px solid var(--border-color); }
|
||||
.smart-options { display: none; background: #ffffff; padding: 16px; border: 1px solid var(--border-color); margin-top: 12px; }
|
||||
.smart-options.active { display: block; }
|
||||
.broadcast-type-selector { display: flex; gap: 15px; margin-bottom: 20px; }
|
||||
.broadcast-type-option { flex: 1; padding: 20px; border: 2px solid #e2e8f0; border-radius: 8px; cursor: pointer; transition: all 0.3s; text-align: center; }
|
||||
.broadcast-type-option:hover { border-color: #667eea; background: #f8f9ff; }
|
||||
.broadcast-type-option.active { border-color: #667eea; background: #eef2ff; }
|
||||
.broadcast-type-option h4 { margin-bottom: 10px; }
|
||||
.broadcast-type-option .icon { font-size: 2.5em; margin-bottom: 10px; }
|
||||
.nav-tabs .nav-link { color: #495057; }
|
||||
.nav-tabs .nav-link.active { font-weight: bold; color: #667eea; }
|
||||
.broadcast-item { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
.badge-smart { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
|
||||
.badge-regular { background: #6c757d; color: white; }
|
||||
.progress-mini { width: 100px; height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; display: inline-block; vertical-align: middle; margin-left: 5px; }
|
||||
.progress-mini-fill { height: 100%; background: linear-gradient(90deg, #48bb78, #38a169); }
|
||||
.readers-table { font-size: 0.9em; }
|
||||
.readers-table th { background: #f8f9fa; }
|
||||
.broadcast-type-selector { display: flex; gap: 12px; margin-bottom: 16px; }
|
||||
.broadcast-type-option { flex: 1; padding: 14px; border: 1px solid var(--border-color); background: #ffffff; cursor: pointer; text-align: center; }
|
||||
.broadcast-type-option:hover { background: #f0f0f0; }
|
||||
.broadcast-type-option.active { border: 2px solid #0000cc; background: #ece9d8; font-weight: bold; }
|
||||
.broadcast-type-option h4 { margin-bottom: 6px; font-size: 14px; }
|
||||
.broadcast-type-option .icon { font-size: 20px; margin-bottom: 6px; }
|
||||
.nav-tabs .nav-link { color: var(--text-main); border-radius: 0; }
|
||||
.nav-tabs .nav-link.active { font-weight: bold; border-color: var(--border-color) var(--border-color) #ffffff; }
|
||||
.broadcast-item { background: white; padding: 12px; border: 1px solid var(--border-color); margin-bottom: 10px; }
|
||||
.badge-smart { background: #e2f0d9; border: 1px solid #70ad47; color: #385723; font-weight: bold; }
|
||||
.badge-regular { background: #e8e8e8; border: 1px solid var(--border-color); color: #111111; }
|
||||
.progress-mini { width: 100px; height: 8px; background: #e8e8e8; border: 1px solid var(--border-light); display: inline-block; vertical-align: middle; margin-left: 5px; }
|
||||
.progress-mini-fill { height: 100%; background: #385723; }
|
||||
.readers-table { font-size: 12px; }
|
||||
.readers-table th { background: var(--bg-header); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -76,8 +76,8 @@
|
|||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-info">🖼️ Использовать готовое фото:</label>
|
||||
<select class="form-select bg-dark text-info border-info" id="broadcastArtifactPhoto">
|
||||
<label class="form-label">🖼️ Использовать готовое фото (Артефакт):</label>
|
||||
<select class="form-select" id="broadcastArtifactPhoto">
|
||||
<option value="">-- Не выбрано (или загрузить новое) --</option>
|
||||
{% for a in artifacts if a.file_type == 'photo' %}
|
||||
<option value="{{ a.file_id }}">{{ a.filename }}</option>
|
||||
|
|
@ -87,8 +87,8 @@
|
|||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-warning">📄 Использовать готовый документ:</label>
|
||||
<select class="form-select bg-dark text-warning border-warning" id="broadcastArtifactDocument">
|
||||
<label class="form-label">📄 Использовать готовый документ (Артефакт):</label>
|
||||
<select class="form-select" id="broadcastArtifactDocument">
|
||||
<option value="">-- Не выбрано (или загрузить новое) --</option>
|
||||
{% for a in artifacts if a.file_type == 'document' %}
|
||||
<option value="{{ a.file_id }}">{{ a.filename }}</option>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,41 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}LKM37 Dashboard{% endblock %}
|
||||
{% block title %}Дашборд - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_dashboard %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h2 style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
||||
>_ ТЕРМИНАЛ_УПРАВЛЕНИЯ_ЛКМ37
|
||||
</h2>
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Панель управления МКД ЛенКом 37</h4>
|
||||
<div class="small text-muted">Сводные показатели системы и текущий статус сервисов</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="row g-3">
|
||||
<!-- КАРТОЧКА: ЖИЛЬЦЫ -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">ЖИЛЬЦЫ</div>
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-people-fill display-4 mb-2"></i>
|
||||
<h5 class="card-title opacity-50">СУБЪЕКТЫ</h5>
|
||||
<h2 class="display-6">{{ users_count }}</h2>
|
||||
<i class="bi bi-people mb-2" style="font-size: 28px; color: #555555;"></i>
|
||||
<div class="small text-muted">Всего зарегистрировано</div>
|
||||
<div style="font-size: 24px; font-weight: bold; margin-top: 4px;">{{ users_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- КАРТОЧКА: ВЕРИФИКАЦИЯ -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 {% if pending_count > 0 %}border-danger shadow-danger{% endif %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">ВЕРИФИКАЦИЯ</div>
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-shield-lock-fill display-4 mb-2"></i>
|
||||
<h5 class="card-title opacity-50">ВЕРИФИКАЦИЯ</h5>
|
||||
<h2 class="display-6">{{ pending_count }}</h2>
|
||||
<i class="bi bi-shield-lock mb-2" style="font-size: 28px; color: #555555;"></i>
|
||||
<div class="small text-muted">Заявок на проверку</div>
|
||||
<div style="font-size: 24px; font-weight: bold; margin-top: 4px;">{{ pending_count }}</div>
|
||||
{% if pending_count > 0 %}
|
||||
<span class="badge bg-danger">ТРЕБУЕТ ВНИМАНИЯ</span>
|
||||
<div class="mt-2">
|
||||
<span class="badge-status badge-danger">ТРЕБУЕТ ВНИМАНИЯ</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -40,54 +43,79 @@
|
|||
|
||||
<!-- КАРТОЧКА: ОБЪЯВЛЕНИЯ -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-warning">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">ОБЪЯВЛЕНИЯ</div>
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-shop display-4 mb-2"></i>
|
||||
<h5 class="card-title opacity-50">МАРКЕТ</h5>
|
||||
<h2 class="display-6 text-warning">{{ active_ads }}</h2>
|
||||
<i class="bi bi-shop mb-2" style="font-size: 28px; color: #555555;"></i>
|
||||
<div class="small text-muted">Активные публикации</div>
|
||||
<div style="font-size: 24px; font-weight: bold; margin-top: 4px;">{{ active_ads }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- КАРТОЧКА: АКТИВНОСТЬ -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-cyan">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">СООБЩЕНИЯ</div>
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-activity display-4 mb-2"></i>
|
||||
<h5 class="card-title opacity-50">СООБЩЕНИЯ</h5>
|
||||
<h2 class="display-6" style="color: var(--matrix-cyan);">{{ messages_count }}</h2>
|
||||
<i class="bi bi-activity mb-2" style="font-size: 28px; color: #555555;"></i>
|
||||
<div class="small text-muted">Активность чата</div>
|
||||
<div style="font-size: 24px; font-weight: bold; margin-top: 4px;">{{ messages_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="row mt-3 g-3">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">СИСТЕМНЫЙ_СТАТУС</div>
|
||||
<div class="card h-100">
|
||||
<div class="card-header">СИСТЕМНЫЙ СТАТУС</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-success font-monospace small">
|
||||
[OK] Database: PostgreSQL (Synology NAS)<br>
|
||||
[OK] Telegram: @lkmthreeseven_bot<br>
|
||||
[OK] Proxy: Active (127.0.0.1:10808)<br>
|
||||
[OK] Node: {{ username }}@server<br>
|
||||
<hr class="border-success">
|
||||
[INFO] Всего верифицировано: {{ verified_count }} жильцов.<br>
|
||||
[INFO] Активных опросов: {{ active_polls }}<br>
|
||||
[INFO] Предстоящих событий: {{ upcoming_events }}
|
||||
</div>
|
||||
<table class="table mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 220px; font-weight: bold;">База данных:</td>
|
||||
<td>PostgreSQL (Synology NAS 192.168.10.105:5433) <span class="badge-status badge-success">OK</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Telegram-бот:</td>
|
||||
<td>@lkmthreeseven_bot (DomovoyKontrolBot) <span class="badge-status badge-success">ONLINE</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Юридический модуль:</td>
|
||||
<td>Google NotebookLM (аккаунт corteskelsey814, 14 источников) <span class="badge-status badge-success">READY</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Текущий узел:</td>
|
||||
<td>{{ username }}@DockerServer (192.168.10.116)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Верифицировано жильцов:</td>
|
||||
<td>{{ verified_count }} чел.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Активных опросов:</td>
|
||||
<td>{{ active_polls }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Предстоящих событий:</td>
|
||||
<td>{{ upcoming_events }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">БЫСТРЫЕ_ДЕЙСТВИЯ</div>
|
||||
<div class="card-body">
|
||||
<a href="/broadcast" class="btn btn-matrix w-100 mb-2">НОВАЯ РАССЫЛКА</a>
|
||||
<a href="/verification" class="btn btn-outline-success w-100 btn-sm mb-2">ПРОВЕРИТЬ ЗАЯВКИ</a>
|
||||
<form action="/api/digest/generate" method="post">
|
||||
<button type="submit" class="btn btn-outline-info w-100 btn-sm">ДАЙДЖЕСТ (ТЕСТ)</button>
|
||||
<div class="card h-100">
|
||||
<div class="card-header">ДЕЙСТВИЯ</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<a href="/broadcast" class="btn-unix btn-unix-primary text-center">Новая рассылка</a>
|
||||
<a href="/verification" class="btn-unix text-center">Проверить заявки жильцов</a>
|
||||
<a href="/legal" class="btn-unix text-center">База законов и NotebookLM</a>
|
||||
<form action="/api/digest/generate" method="post" class="mt-auto">
|
||||
<button type="submit" class="btn-unix w-100">Тестовая генерация дайджеста</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,98 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Настройки дайджеста - Домовой Бот{% endblock %}
|
||||
|
||||
{% block nav_digest_settings %}active{% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
<style>
|
||||
.digest-settings-container { max-width: 900px; }
|
||||
.digest-header { background: linear-gradient(135deg, var(--color-primary) 0%, rgba(10, 10, 15, 0.7) 100%); border: 1px solid var(--panel-border); color: white; padding: 30px; border-radius: 16px; margin-bottom: 30px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
|
||||
.digest-header h1 { font-size: 2em; margin-bottom: 10px; text-shadow: 0 0 10px var(--color-primary-glow); }
|
||||
.digest-back-link { color: var(--color-success); text-decoration: none; opacity: 0.9; transition: var(--transition-smooth); }
|
||||
.digest-back-link:hover { opacity: 1; text-shadow: 0 0 8px var(--color-success-glow); }
|
||||
.digest-card { background: var(--panel-bg); border: 1px solid var(--panel-border); padding: 25px; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); transition: var(--transition-smooth); }
|
||||
.digest-card:hover { border-color: var(--panel-border-hover); background: var(--panel-bg-hover); }
|
||||
.digest-card h3 { margin-bottom: 15px; color: var(--color-success); text-shadow: 0 0 5px var(--color-success-glow); }
|
||||
.digest-form-group { margin-bottom: 20px; }
|
||||
.digest-form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: var(--color-text-main); }
|
||||
.digest-form-group input[type="time"], .digest-form-group select { width: 100%; padding: 10px; border: 1px solid var(--panel-border); border-radius: 8px; background: rgba(0, 0, 0, 0.3); color: var(--color-text-main); font-size: 1em; transition: var(--transition-smooth); }
|
||||
.digest-form-group input[type="time"]:focus, .digest-form-group select:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 8px var(--color-primary-glow); }
|
||||
.digest-checkbox-group { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; color: var(--color-text-main); }
|
||||
.digest-checkbox-group input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--color-success); cursor: pointer; }
|
||||
.digest-btn { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-size: 1em; background: var(--color-success); color: #000; font-weight: 600; transition: var(--transition-smooth); }
|
||||
.digest-btn:hover { background: #00cca3; box-shadow: 0 0 10px var(--color-success-glow); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Настройки дайджеста - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_digests %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="digest-settings-container">
|
||||
<div class="digest-header">
|
||||
<a href="/digests" class="digest-back-link">← Назад к дайджестам</a>
|
||||
<h1>⚙️ Настройки дайджеста</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Настройки генерации дайджеста</h4>
|
||||
<div class="small text-muted">Конфигурация автоматического сбора событий и публикации</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/digests" class="btn-unix">← Назад к дайджестам</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="digest-card">
|
||||
<h3>📅 Расписание</h3>
|
||||
<div class="digest-form-group">
|
||||
<label>Автоматическая генерация</label>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="autoGenerate" checked>
|
||||
<label for="autoGenerate">Включено (каждое воскресенье в 19:00)</label>
|
||||
<div class="row g-3" style="max-width: 900px;">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">РАСПИСАНИЕ И АВТОМАТИЗАЦИЯ</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="autoGenerate" checked>
|
||||
<label class="form-check-label" for="autoGenerate">
|
||||
<b>Автоматическая генерация:</b> каждое воскресенье в 19:00 (время Ульяновск)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="digest-form-group">
|
||||
<label>Время генерации</label>
|
||||
<input type="time" value="19:00" disabled>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">РАЗДЕЛЫ ДАЙДЖЕСТА</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="includeEvents" checked>
|
||||
<label class="form-check-label" for="includeEvents">События и встречи дома</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="includePolls" checked>
|
||||
<label class="form-check-label" for="includePolls">Опросы и голосования собственников</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="includeAds" checked>
|
||||
<label class="form-check-label" for="includeAds">Доска объявлений</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="includeTopUsers" checked>
|
||||
<label class="form-check-label" for="includeTopUsers">Топ активных жильцов</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="includeSchedule" checked>
|
||||
<label class="form-check-label" for="includeSchedule">График плановых отключений ЖКХ</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="includeVerifications" checked>
|
||||
<label class="form-check-label" for="includeVerifications">Статистика новых верификаций</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="includeStats" checked>
|
||||
<label class="form-check-label" for="includeStats">Сводная статистика активности чата</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="digest-card">
|
||||
<h3>📊 Содержимое дайджеста</h3>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includeEvents" checked>
|
||||
<label for="includeEvents">🔔 События</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includePolls" checked>
|
||||
<label for="includePolls">📊 Опросы</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includeAds" checked>
|
||||
<label for="includeAds">📢 Объявления</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includeTopUsers" checked>
|
||||
<label for="includeTopUsers">🏆 Топ активных жильцов</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includeSchedule" checked>
|
||||
<label for="includeSchedule">⏰ Запланированные отключения</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includeVerifications" checked>
|
||||
<label for="includeVerifications">✅ Верификации</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="includeStats" checked>
|
||||
<label for="includeStats">📈 Статистика чата</label>
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">РЕЖИМ ПУБЛИКАЦИИ</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="sendPreview" checked>
|
||||
<label class="form-check-label" for="sendPreview">Отправлять превью председателю на утверждение перед рассылкой</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="autoSend">
|
||||
<label class="form-check-label" for="autoSend">Публиковать напрямую без ручного подтверждения</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="saveArchive" checked>
|
||||
<label class="form-check-label" for="saveArchive">Сохранять выпуск в архив базы данных</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="digest-card">
|
||||
<h3>📬 Отправка</h3>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="sendPreview" checked>
|
||||
<label for="sendPreview">Отправлять превью админу для утверждения</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="autoSend">
|
||||
<label for="autoSend">Публиковать автоматически (без утверждения)</label>
|
||||
</div>
|
||||
<div class="digest-checkbox-group">
|
||||
<input type="checkbox" id="saveArchive" checked>
|
||||
<label for="saveArchive">Сохранять в архив</label>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn-unix btn-unix-primary" onclick="saveSettings()">Сохранить настройки</button>
|
||||
</div>
|
||||
|
||||
<button class="digest-btn" onclick="saveSettings()">💾 Сохранить настройки</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script>
|
||||
function saveSettings() {
|
||||
alert('✅ Настройки сохранены!');
|
||||
alert('Настройки успешно сохранены');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,275 +1,250 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Еженедельные дайджесты - Домовой Бот{% endblock %}
|
||||
|
||||
{% block title %}Еженедельные дайджесты - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_digests %}active{% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, var(--color-primary) 0%, rgba(10, 10, 15, 0.7) 100%); border: 1px solid var(--panel-border); color: white; padding: 30px; border-radius: 16px; margin-bottom: 30px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
|
||||
.header h1 { font-size: 2em; margin-bottom: 10px; text-shadow: 0 0 10px var(--color-primary-glow); }
|
||||
.back-link { color: var(--color-success); text-decoration: none; opacity: 0.9; }
|
||||
.back-link:hover { opacity: 1; text-shadow: 0 0 8px var(--color-success-glow); }
|
||||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; }
|
||||
.tab { padding: 12px 24px; background: rgba(255, 255, 255, 0.05); color: var(--color-text-muted); border: 1px solid var(--panel-border); border-radius: 8px 8px 0 0; cursor: pointer; font-size: 1em; transition: var(--transition-smooth); }
|
||||
.tab:hover { color: var(--color-text-main); background: rgba(255, 255, 255, 0.08); }
|
||||
.tab.active { background: var(--color-primary); color: white; border-color: var(--color-primary); box-shadow: 0 0 10px var(--color-primary-glow); }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.pending-card { background: var(--panel-bg); padding: 25px; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); border: 1px solid var(--panel-border); border-left: 5px solid var(--color-primary); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); transition: var(--transition-smooth); }
|
||||
.pending-card:hover { border-color: var(--panel-border-hover); background: var(--panel-bg-hover); }
|
||||
.pending-card h3 { color: var(--color-success); margin-bottom: 10px; text-shadow: 0 0 5px var(--color-success-glow); }
|
||||
.pending-card .meta { color: var(--color-text-muted); font-size: 0.9em; margin-bottom: 15px; }
|
||||
.pending-card .text { background: rgba(0, 0, 0, 0.3); color: var(--color-text-main); border: 1px solid var(--panel-border); padding: 15px; border-radius: 8px; margin: 15px 0; white-space: pre-wrap; max-height: 300px; overflow-y: auto; font-family: var(--font-mono); }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 0.95em; margin-right: 10px; transition: var(--transition-smooth); }
|
||||
.btn-approve { background: var(--color-success); color: #000; font-weight: 600; }
|
||||
.btn-approve:hover { background: #00cca3; box-shadow: 0 0 10px var(--color-success-glow); }
|
||||
.btn-reject { background: var(--color-danger); color: white; }
|
||||
.btn-reject:hover { background: #e0244a; box-shadow: 0 0 10px var(--color-danger-glow); }
|
||||
.btn-edit { background: var(--color-warning); color: #000; font-weight: 600; }
|
||||
.btn-edit:hover { background: #d99800; box-shadow: 0 0 10px var(--color-warning-glow); }
|
||||
.btn-send { background: var(--color-info); color: #000; font-weight: 600; }
|
||||
.btn-send:hover { background: #0099cc; box-shadow: 0 0 10px var(--color-info-glow); }
|
||||
table { width: 100%; background: var(--panel-bg); border-radius: 16px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.2); border: 1px solid var(--panel-border); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }
|
||||
th { background: rgba(138, 43, 226, 0.2); color: var(--color-success); padding: 15px; text-align: left; border-bottom: 2px solid var(--panel-border); font-weight: 600; text-transform: uppercase; font-size: 0.85em; letter-spacing: 0.5px; }
|
||||
td { padding: 15px; border-bottom: 1px solid var(--panel-border); color: var(--color-text-main); }
|
||||
tr:hover { background: rgba(255, 255, 255, 0.02); }
|
||||
.badge { display: inline-block; padding: 4px 8px; border-radius: 6px; font-size: 0.85em; font-weight: 500; border: 1px solid transparent; }
|
||||
.badge-pending { background: rgba(255, 179, 0, 0.15); color: var(--color-warning); border-color: var(--color-warning); }
|
||||
.badge-approved { background: rgba(0, 255, 204, 0.15); color: var(--color-success); border-color: var(--color-success); }
|
||||
.badge-rejected { background: rgba(255, 45, 85, 0.15); color: var(--color-danger); border-color: var(--color-danger); }
|
||||
.badge-sent { background: rgba(0, 191, 255, 0.15); color: var(--color-info); border-color: var(--color-info); }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; backdrop-filter: blur(8px); }
|
||||
.modal.active { display: flex; align-items: center; justify-content: center; }
|
||||
.modal-content { background: var(--bg-main); border: 1px solid var(--panel-border); color: var(--color-text-main); padding: 30px; border-radius: 16px; max-width: 900px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 15px var(--color-primary-glow); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid var(--panel-border); padding-bottom: 15px; }
|
||||
.close-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; color: var(--color-text-muted); }
|
||||
.close-btn:hover { color: var(--color-text-main); }
|
||||
textarea { width: 100%; min-height: 300px; padding: 15px; border: 1px solid var(--panel-border); border-radius: 8px; background: rgba(0, 0, 0, 0.3); color: var(--color-text-main); font-family: var(--font-mono); font-size: 0.95em; transition: var(--transition-smooth); }
|
||||
textarea:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 8px var(--color-primary-glow); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<a href="/" class="back-link">← На главную</a>
|
||||
<h1>📰 Еженедельные дайджесты</h1>
|
||||
<p>Утверждение, редактирование и архив</p>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Еженедельные дайджесты дома</h4>
|
||||
<div class="small text-muted">Утверждение проектов, ручная корректировка и архив публикаций</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn-unix btn-unix-primary" onclick="generateNow()">
|
||||
<i class="bi bi-arrow-repeat"></i> Сгенерировать сейчас
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showTab('pending')">⏳ Ожидают утверждения</button>
|
||||
<button class="tab" onclick="showTab('archive')">📚 Архив</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn-unix active" id="tabBtnPending" onclick="showTab('pending')">Ожидают утверждения ({{ pending_digests|length }})</button>
|
||||
<button class="btn-unix" id="tabBtnArchive" onclick="showTab('archive')">Архив дайджестов</button>
|
||||
</div>
|
||||
|
||||
<div id="pending" class="tab-content active">
|
||||
{% if pending_digests|length > 0 %}
|
||||
{% for digest in pending_digests %}
|
||||
<div class="pending-card">
|
||||
<h3>📰 Дайджест #{{ digest.week_number }}/{{ digest.year }}</h3>
|
||||
<div class="meta">
|
||||
Период: {{ digest.period_label }} | Создан: {{ digest.created_at }} | Статус: <span class="badge badge-pending">{{ digest.status_emoji }} {{ digest.status }}</span>
|
||||
</div>
|
||||
<div class="text">{{ digest.digest_text }}</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button class="btn btn-approve" onclick="approveDigest({{ digest.id }})">✅ Утвердить</button>
|
||||
<button class="btn btn-edit" onclick="editDigest({{ digest.id }})">✏️ Редактировать</button>
|
||||
<button class="btn btn-reject" onclick="rejectDigest({{ digest.id }})">❌ Отклонить</button>
|
||||
<button class="btn btn-send" onclick="sendDigest({{ digest.id }})" id="sendBtn{{ digest.id }}" style="display:none;">📨 Отправить</button>
|
||||
</div>
|
||||
<!-- ВКЛАДКА: НА УТВЕРЖДЕНИИ -->
|
||||
<div id="pending" class="tab-content-panel">
|
||||
{% if pending_digests|length > 0 %}
|
||||
{% for digest in pending_digests %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Дайджест #{{ digest.week_number }}/{{ digest.year }} (Период: {{ digest.period_label }})</span>
|
||||
<span class="badge-status badge-warning">{{ digest.status_emoji }} {{ digest.status.upper() }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small text-muted mb-2">Создан: {{ digest.created_at }}</div>
|
||||
<pre style="white-space: pre-wrap; max-height: 250px; background-color: #ffffff; border: 1px solid var(--border-color);">{{ digest.digest_text }}</pre>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn-unix btn-unix-primary" onclick="approveDigest({{ digest.id }})">Утвердить</button>
|
||||
<button class="btn-unix" onclick="editDigest({{ digest.id }})">Редактировать</button>
|
||||
<button class="btn-unix" onclick="rejectDigest({{ digest.id }})">Отклонить</button>
|
||||
<button class="btn-unix" onclick="sendDigest({{ digest.id }})" id="sendBtn{{ digest.id }}" style="display:none;">Отправить жильцам</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 50px; background: white; border-radius: 8px;">
|
||||
<h2>📭 Нет ожидающих утверждения дайджестов</h2>
|
||||
<p style="margin-top: 10px;">Следующий дайджест будет сгенерирован в воскресенье в 09:00</p>
|
||||
<button class="btn btn-send" style="margin-top: 20px;" onclick="generateNow()">🔄 Сгенерировать сейчас</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="archive" class="tab-content">
|
||||
{% if archive_digests|length > 0 %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Неделя</th>
|
||||
<th>Период</th>
|
||||
<th>Статус</th>
|
||||
<th>Отправлен</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for digest in archive_digests %}
|
||||
<tr>
|
||||
<td>#{{ digest.week_number }}/{{ digest.year }}</td>
|
||||
<td>{{ digest.period_label }}</td>
|
||||
<td><span class="badge badge-{{ digest.status }}">{{ digest.status_emoji }} {{ digest.status }}</span></td>
|
||||
<td>{{ digest.sent_at }}</td>
|
||||
<td>
|
||||
<button class="btn btn-edit" onclick="viewDigest({{ digest.id }})">👁️ Просмотр</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 50px; background: white; border-radius: 8px;">
|
||||
<h2>📚 Архив пуст</h2>
|
||||
<p style="margin-top: 10px;">Здесь будут отображаться все дайджесты</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="card p-4 text-center">
|
||||
<div class="mb-2"><i class="bi bi-inbox text-muted" style="font-size: 32px;"></i></div>
|
||||
<div style="font-weight: bold;">Нет дайджестов, ожидающих утверждения</div>
|
||||
<div class="small text-muted mt-1">Автоматическая генерация запускается по расписанию каждое воскресенье в 19:00</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для редактирования -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>✏️ Редактирование дайджеста</h2>
|
||||
<button class="close-btn" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<textarea id="editText"></textarea>
|
||||
<div style="margin-top: 15px;">
|
||||
<button class="btn btn-approve" onclick="saveEdit()">💾 Сохранить</button>
|
||||
<button class="btn btn-reject" onclick="closeEditModal()">Отмена</button>
|
||||
<!-- ВКЛАДКА: АРХИВ -->
|
||||
<div id="archive" class="tab-content-panel" style="display: none;">
|
||||
{% if archive_digests|length > 0 %}
|
||||
<div class="card">
|
||||
<div class="card-header">АРХИВ ВЫПУСКОВ</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 120px;">Выпуск</th>
|
||||
<th>Период</th>
|
||||
<th style="width: 140px;">Статус</th>
|
||||
<th style="width: 180px;">Отправлен</th>
|
||||
<th style="width: 100px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for digest in archive_digests %}
|
||||
<tr>
|
||||
<td><b>#{{ digest.week_number }}/{{ digest.year }}</b></td>
|
||||
<td>{{ digest.period_label }}</td>
|
||||
<td>
|
||||
<span class="badge-status {% if digest.status == 'approved' or digest.status == 'sent' %}badge-success{% elif digest.status == 'rejected' %}badge-danger{% else %}badge-warning{% endif %}">
|
||||
{{ digest.status_emoji }} {{ digest.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ digest.sent_at or '—' }}</td>
|
||||
<td>
|
||||
<button class="btn-unix" onclick="viewDigest({{ digest.id }})">Открыть</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card p-4 text-center">
|
||||
<div class="small text-muted">Архив пока пуст</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра -->
|
||||
<div id="viewModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="viewTitle">📰 Дайджест</h2>
|
||||
<button class="close-btn" onclick="closeViewModal()">×</button>
|
||||
<!-- Модальное окно для редактирования -->
|
||||
<div id="editModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1050; align-items: center; justify-content: center;">
|
||||
<div class="card" style="width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; margin: 40px auto; border: 2px solid var(--border-color);">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>РЕДАКТИРОВАНИЕ ДАЙДЖЕСТА</span>
|
||||
<button class="btn-unix" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<textarea id="editText" class="form-control" style="width: 100%; min-height: 350px; font-family: inherit; font-size: 12px;"></textarea>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn-unix btn-unix-primary" onclick="saveEdit()">Сохранить изменения</button>
|
||||
<button class="btn-unix" onclick="closeEditModal()">Отмена</button>
|
||||
</div>
|
||||
<div id="viewText" style="background: #f8f9fa; padding: 15px; border-radius: 5px; white-space: pre-wrap;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentDigestId = null;
|
||||
<!-- Модальное окно для просмотра -->
|
||||
<div id="viewModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1050; align-items: center; justify-content: center;">
|
||||
<div class="card" style="width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; margin: 40px auto; border: 2px solid var(--border-color);">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span id="viewTitle">ПРОСМОТР ДАЙДЖЕСТА</span>
|
||||
<button class="btn-unix" onclick="closeViewModal()">×</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre id="viewText" style="white-space: pre-wrap; background-color: #ffffff; border: 1px solid var(--border-color);"></pre>
|
||||
<div class="mt-3">
|
||||
<button class="btn-unix" onclick="closeViewModal()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
function showTab(tabName) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
<script>
|
||||
let currentDigestId = null;
|
||||
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
}
|
||||
function showTab(tabName) {
|
||||
document.getElementById('pending').style.display = tabName === 'pending' ? 'block' : 'none';
|
||||
document.getElementById('archive').style.display = tabName === 'archive' ? 'block' : 'none';
|
||||
document.getElementById('tabBtnPending').className = tabName === 'pending' ? 'btn-unix active' : 'btn-unix';
|
||||
document.getElementById('tabBtnArchive').className = tabName === 'archive' ? 'btn-unix active' : 'btn-unix';
|
||||
}
|
||||
|
||||
function approveDigest(id) {
|
||||
if (confirm('Утвердить этот дайджест?')) {
|
||||
fetch(`/api/digest/${id}/approve`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
// Показываем кнопку "Отправить"
|
||||
document.getElementById(`sendBtn${id}`).style.display = 'inline-block';
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function rejectDigest(id) {
|
||||
const reason = prompt('Причина отклонения:');
|
||||
if (reason) {
|
||||
fetch(`/api/digest/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason: reason })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function editDigest(id) {
|
||||
currentDigestId = id;
|
||||
fetch(`/api/digest/${id}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('editText').value = data.digest.text;
|
||||
document.getElementById('editModal').classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
const newText = document.getElementById('editText').value;
|
||||
fetch(`/api/digest/${currentDigestId}/update_text`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: newText })
|
||||
})
|
||||
function approveDigest(id) {
|
||||
if (confirm('Утвердить этот дайджест?')) {
|
||||
fetch(`/api/digest/${id}/approve`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
closeEditModal();
|
||||
const sendBtn = document.getElementById(`sendBtn${id}`);
|
||||
if (sendBtn) sendBtn.style.display = 'inline-block';
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function rejectDigest(id) {
|
||||
const reason = prompt('Причина отклонения:');
|
||||
if (reason) {
|
||||
fetch(`/api/digest/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason: reason })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function editDigest(id) {
|
||||
currentDigestId = id;
|
||||
fetch(`/api/digest/${id}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('editText').value = data.digest.text;
|
||||
document.getElementById('editModal').style.display = 'flex';
|
||||
});
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
const newText = document.getElementById('editText').value;
|
||||
fetch(`/api/digest/${currentDigestId}/update_text`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: newText })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
closeEditModal();
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('editModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function viewDigest(id) {
|
||||
fetch(`/api/digest/${id}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('viewTitle').textContent = `Дайджест #${data.digest.week}/${data.digest.year}`;
|
||||
document.getElementById('viewText').textContent = data.digest.text;
|
||||
document.getElementById('viewModal').style.display = 'flex';
|
||||
});
|
||||
}
|
||||
|
||||
function closeViewModal() {
|
||||
document.getElementById('viewModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function sendDigest(id) {
|
||||
if (confirm('Отправить дайджест всем верифицированным жильцам?')) {
|
||||
fetch(`/api/digest/${id}/send`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateNow() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
const oldText = btn.innerHTML;
|
||||
btn.innerHTML = '⏳ Генерация...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/digest/generate', { method: 'POST' });
|
||||
const result = await r.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Успешно: ' + result.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка: ' + (result.error || 'Неизвестная ошибка'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = oldText;
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('editModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function viewDigest(id) {
|
||||
fetch(`/api/digest/${id}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('viewTitle').textContent = `📰 Дайджест #${data.digest.week}/${data.digest.year}`;
|
||||
document.getElementById('viewText').textContent = data.digest.text;
|
||||
document.getElementById('viewModal').classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
function closeViewModal() {
|
||||
document.getElementById('viewModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function sendDigest(id) {
|
||||
if (confirm('Отправить дайджест всем верифицированным жильцам?')) {
|
||||
fetch(`/api/digest/${id}/send`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateNow() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Генерация...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/digest/generate', { method: 'POST' });
|
||||
const result = await r.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✅ ' + result.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ Ошибка: ' + (result.error || 'Неизвестная'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔄 Сгенерировать сейчас';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('❌ Ошибка: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔄 Сгенерировать сейчас';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
} catch (err) {
|
||||
alert('Ошибка: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = oldText;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,72 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Email Audit - LKM37 Admin{% endblock %}
|
||||
{% block title %}Реестр писем и аудит - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_emails %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- ЗАГОЛОВОК -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2 style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
||||
>_ АУДИТ_ВЗАИМОДЕЙСТВИЯ_С_ОРГАНАМИ
|
||||
</h2>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Реестр официальной переписки и судебный аудит</h4>
|
||||
<div class="small text-muted">Архив писем, обращений и ответов надзорных органов дома № 37</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<span class="badge bg-success p-2">АВТОМАТИЗАЦИЯ_CRON: ACTIVE (04:00)</span>
|
||||
<div>
|
||||
<span class="badge-status badge-success">CRON: ACTIVE (04:00)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- МИНИ-КАРТОЧКИ АНАЛИТИКИ (ВИДЖЕТЫ) -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- КАРТОЧКА 1: ВСЕГО ПИСЕМ -->
|
||||
<!-- МИНИ-КАРТОЧКИ АНАЛИТИКИ -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-success h-100 shadow">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>ВСЕГО ПИСЕМ</span>
|
||||
<i class="bi bi-envelope-open"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="text-success text-uppercase small font-monospace mb-0">>_ ВСЕГО_ПИСЕМ</h6>
|
||||
<i class="bi bi-envelope-open text-success fs-4"></i>
|
||||
</div>
|
||||
<h2 class="display-6 font-monospace my-2 text-white" id="statTotalEmails">{{ total_emails }}</h2>
|
||||
<div class="small text-muted">
|
||||
Вх: <span id="statInboundEmails" class="text-success fw-bold">{{ total_inbound }}</span> /
|
||||
Исх: <span id="statOutboundEmails" class="text-info fw-bold">{{ total_outbound }}</span>
|
||||
<div style="font-size: 22px; font-weight: bold;" id="statTotalEmails">{{ total_emails }}</div>
|
||||
<div class="small text-muted mt-1">
|
||||
Входящие: <b id="statInboundEmails">{{ total_inbound }}</b> / Исходящие: <b id="statOutboundEmails">{{ total_outbound }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- КАРТОЧКА 2: ОБЪЕМ АРХИВА -->
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-info h-100 shadow">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>ОБЪЕМ АРХИВА</span>
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="text-info text-uppercase small font-monospace mb-0">>_ ОБЪЕМ_АРХИВА</h6>
|
||||
<i class="bi bi-file-earmark-pdf text-info fs-4"></i>
|
||||
</div>
|
||||
<h2 class="display-6 font-monospace my-2 text-white" id="statPdfSize">{{ pdf_size_mb }} MB</h2>
|
||||
<div class="small text-muted text-truncate" id="statLastUpdate">
|
||||
<div style="font-size: 22px; font-weight: bold;" id="statPdfSize">{{ pdf_size_mb }} MB</div>
|
||||
<div class="small text-muted mt-1 text-truncate" id="statLastUpdate">
|
||||
Создан: {{ fmt_local(last_update) if last_update else '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- КАРТОЧКА 3: ВСЕГО ВЛОЖЕНИЙ -->
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-warning h-100 shadow">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>ВЛОЖЕНИЯ</span>
|
||||
<i class="bi bi-paperclip"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="text-warning text-uppercase small font-monospace mb-0">>_ ВШИТЫХ_ВЛОЖЕНИЙ</h6>
|
||||
<i class="bi bi-paperclip text-warning fs-4"></i>
|
||||
</div>
|
||||
<h2 class="display-6 font-monospace my-2 text-white" id="statAttachments">{{ total_attachments }}</h2>
|
||||
<div class="small text-muted">Интегрировано в листы PDF</div>
|
||||
<div style="font-size: 22px; font-weight: bold;" id="statAttachments">{{ total_attachments }}</div>
|
||||
<div class="small text-muted mt-1">Интегрировано в PDF</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- КАРТОЧКА 4: ПОСЛЕДНЯЯ СИНХРОНИЗАЦИЯ -->
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-primary h-100 shadow">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>DRIVE СИНХРОНИЗАЦИЯ</span>
|
||||
<i class="bi bi-cloud-check"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="text-primary text-uppercase small font-monospace mb-0">>_ DRIVE_SYNC</h6>
|
||||
<i class="bi bi-google-play text-primary fs-4"></i>
|
||||
</div>
|
||||
<h2 class="display-6 font-monospace my-2 text-white" id="statLastSync">
|
||||
<div style="font-size: 22px; font-weight: bold;" id="statLastSync">
|
||||
{% if last_sync_time %}{{ last_sync_time.strftime('%d.%m.%Y') }}{% else %}-{% endif %}
|
||||
</h2>
|
||||
<div class="small text-muted text-truncate" id="statLastSyncTime">
|
||||
</div>
|
||||
<div class="small text-muted mt-1 text-truncate" id="statLastSyncTime">
|
||||
Время: {{ fmt_local(last_sync_time) if last_sync_time else 'Неизвестно' }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,114 +74,149 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- ЛЕВАЯ КОЛОНКА: УПРАВЛЕНИЕ И СТАТУС -->
|
||||
<div class="row g-3">
|
||||
<!-- ЛЕВАЯ КОЛОНКА -->
|
||||
<div class="col-md-4">
|
||||
<!-- КАРТОЧКА УПРАВЛЕНИЯ -->
|
||||
<div class="card mb-4 shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||||
[CONTROL] УПРАВЛЕНИЕ_АРХИВОМ
|
||||
</div>
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">УПРАВЛЕНИЕ АРХИВОМ</div>
|
||||
<div class="card-body text-center py-3">
|
||||
{% if has_master_pdf %}
|
||||
<i class="bi bi-file-earmark-pdf-fill display-2 text-danger mb-3"></i>
|
||||
<h6 class="text-white">ALL_EMAILS_CONSOLIDATED.pdf</h6>
|
||||
<p class="small opacity-50 mb-4">Архив готов к использованию в NotebookLM</p>
|
||||
<div class="mb-2">
|
||||
<i class="bi bi-file-earmark-pdf text-danger" style="font-size: 36px;"></i>
|
||||
</div>
|
||||
<div style="font-weight: bold;">ALL_EMAILS_CONSOLIDATED.pdf</div>
|
||||
<div class="small text-muted mb-3">Архив готов к работе в NotebookLM</div>
|
||||
|
||||
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||||
<i class="bi bi-lightning-charge-fill"></i> ТУРБО СИНХРОНИЗАЦИЯ
|
||||
<button id="fullSyncBtn" class="btn-unix btn-unix-primary w-100 mb-2">
|
||||
<i class="bi bi-arrow-repeat"></i> Запустить синхронизацию
|
||||
</button>
|
||||
|
||||
<div class="dropdown w-100">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle w-100" type="button" data-bs-toggle="dropdown">
|
||||
ДОПОЛНИТЕЛЬНО
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark w-100">
|
||||
<li><a href="/static/email_archive/TOTAL_ARCHIVE_2025-2026.pdf" class="dropdown-item" download>СКАЧАТЬ АРХИВ</a></li>
|
||||
<li><button id="syncAiBtn" class="dropdown-item">ТОЛЬКО UPLOAD В DRIVE</button></li>
|
||||
</ul>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/static/email_archive/TOTAL_ARCHIVE_2025-2026.pdf" class="btn-unix w-50 text-center" download>Скачать PDF</a>
|
||||
<button id="syncAiBtn" class="btn-unix w-50">В Google Drive</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<i class="bi bi-file-earmark-x display-2 opacity-20 mb-3"></i>
|
||||
<p class="text-warning">Архив еще не сформирован</p>
|
||||
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||||
<i class="bi bi-lightning-charge-fill"></i> ЗАПУСТИТЬ ПЕРВЫЙ СБОР
|
||||
<div class="mb-2">
|
||||
<i class="bi bi-file-earmark-x text-muted" style="font-size: 36px;"></i>
|
||||
</div>
|
||||
<div class="small text-muted mb-3">Архив еще не сформирован</div>
|
||||
<button id="fullSyncBtn" class="btn-unix btn-unix-primary w-100">
|
||||
<i class="bi bi-arrow-repeat"></i> Запустить первый сбор
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- СТАТУС ПАЙПЛАЙНА (ПОСТОЯННЫЙ) -->
|
||||
<div class="card border-primary mb-4 shadow" id="pipelineStatusCard">
|
||||
<div class="card-header bg-dark border-bottom border-primary d-flex justify-content-between align-items-center text-primary font-monospace">
|
||||
<span>[PIPELINE] МОНИТОРИНГ_ПРОЦЕССА</span>
|
||||
<span class="spinner-grow spinner-grow-sm text-primary" id="pipelineSpinner" style="display: none;" role="status"></span>
|
||||
<!-- СТАТУС ПАЙПЛАЙНА -->
|
||||
<div class="card mb-3" id="pipelineStatusCard">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>МОНИТОРИНГ ПРОЦЕССА</span>
|
||||
<span class="spinner-border spinner-border-sm" id="pipelineSpinner" style="display: none;" role="status"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-secondary font-monospace small">
|
||||
[STEP] Шаг: <span id="pipelineStep" class="text-info fw-bold">Ожидание</span> <br>
|
||||
[TIME] Обновлено: <span id="pipelineTime">-</span> <br>
|
||||
<div class="progress my-3" style="height: 12px; background-color: #111;">
|
||||
<div id="pipelineProgress" class="progress-bar progress-bar-striped progress-bar-animated bg-primary" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
[DATA] Новых писем: <span id="pipelineNewEmails" class="text-success fw-bold">0</span> <br>
|
||||
[DATA] Всего в архиве: <span id="pipelineTotalEmails" class="fw-bold">0</span> <br>
|
||||
[DATA] Размер файла: <span id="pipelineSize" class="fw-bold">0.00 MB</span>
|
||||
</div>
|
||||
<table class="table mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 120px; font-weight: bold;">Шаг:</td>
|
||||
<td><span id="pipelineStep">Ожидание</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Обновлено:</td>
|
||||
<td><span id="pipelineTime">-</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Прогресс:</td>
|
||||
<td>
|
||||
<div class="progress" style="height: 10px; background-color: #e8e8e8; border-radius: 0;">
|
||||
<div id="pipelineProgress" class="progress-bar bg-primary" role="progressbar" style="width: 0%; border-radius: 0;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Новых писем:</td>
|
||||
<td><span id="pipelineNewEmails">0</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">В архиве:</td>
|
||||
<td><span id="pipelineTotalEmails">0</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Размер:</td>
|
||||
<td><span id="pipelineSize">0.00 MB</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ЛОГ ПОСЛЕДНЕГО ЗАПУСКА -->
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-warning font-monospace">
|
||||
[LOG] ЛОГ_ПОСЛЕДНЕГО_ЗАПУСКА
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-warning font-monospace small">
|
||||
{% if last_log %}
|
||||
[TIME] Начат: {{ fmt_local(last_log.start_time) }} <br>
|
||||
[TIME] Завершен: {{ fmt_local(last_log.end_time) if last_log.end_time else 'В ПРОЦЕССЕ...' }} <br>
|
||||
[STATUS] Статус: <span class="{% if last_log.status == 'success' %}text-success{% elif last_log.status == 'error' %}text-danger{% else %}text-warning{% endif %} fw-bold">{{ last_log.status.upper() }}</span> <br>
|
||||
[DATA] Новых писем: {{ last_log.new_emails_count }}
|
||||
{% else %}
|
||||
[INFO] Запуски еще не производились.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">ПОСЛЕДНИЙ ЗАПУСК</div>
|
||||
<div class="card-body p-2">
|
||||
<table class="table mb-0">
|
||||
<tbody>
|
||||
{% if last_log %}
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Начат:</td>
|
||||
<td>{{ fmt_local(last_log.start_time) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Завершен:</td>
|
||||
<td>{{ fmt_local(last_log.end_time) if last_log.end_time else 'В процессе...' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Статус:</td>
|
||||
<td>
|
||||
<span class="badge-status {% if last_log.status == 'success' %}badge-success{% elif last_log.status == 'error' %}badge-danger{% else %}badge-warning{% endif %}">
|
||||
{{ last_log.status.upper() }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Новых писем:</td>
|
||||
<td>{{ last_log.new_emails_count }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-muted text-center py-2">Запуски еще не производились</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ПРАВАЯ КОЛОНКА: ТАБЛИЦА ВЕДОМСТВ -->
|
||||
<div class="col-md-8">
|
||||
<div class="card h-100 shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||||
[STATS] СТАТИСТИКА_ПО_ВЕДОМСТВАМ_И_ОРГАНАМ
|
||||
</div>
|
||||
<div class="card h-100">
|
||||
<div class="card-header">СТАТИСТИКА ПО ОРГАНАМ И ВЕДОМСТВАМ</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 700px; overflow-y: auto;">
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead class="sticky-top bg-dark">
|
||||
<div class="table-responsive" style="max-height: 650px; overflow-y: auto;">
|
||||
<table class="table mb-0">
|
||||
<thead style="position: sticky; top: 0; z-index: 1;">
|
||||
<tr>
|
||||
<th>Ведомство / Домен</th>
|
||||
<th class="text-center" style="width: 100px;">Вх.</th>
|
||||
<th class="text-center" style="width: 100px;">Исх.</th>
|
||||
<th style="width: 180px;">Последнее письмо</th>
|
||||
<th class="text-center" style="width: 80px;">Вх.</th>
|
||||
<th class="text-center" style="width: 80px;">Исх.</th>
|
||||
<th style="width: 170px;">Последнее письмо</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain in audit_data %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-success fw-bold">{{ domain.domain }}</span>
|
||||
<br><small class="opacity-50 text-white">{{ domain.description }}</small>
|
||||
<b>{{ domain.domain }}</b>
|
||||
<div class="small text-muted">{{ domain.description }}</div>
|
||||
</td>
|
||||
<td class="text-center font-monospace text-success fw-bold">{{ domain.inbound_count }}</td>
|
||||
<td class="text-center font-monospace text-info fw-bold">{{ domain.outbound_count }}</td>
|
||||
<td class="font-monospace">{{ domain.last_email_date.strftime('%d.%m.%Y %H:%M') if domain.last_email_date else '-' }}</td>
|
||||
<td class="text-center"><b>{{ domain.inbound_count }}</b></td>
|
||||
<td class="text-center"><b>{{ domain.outbound_count }}</b></td>
|
||||
<td>{{ domain.last_email_date.strftime('%d.%m.%Y %H:%M') if domain.last_email_date else '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center opacity-50 py-4">Данных пока нет. Запустите экспорт.</td>
|
||||
<td colspan="4" class="text-center text-muted py-3">Данных пока нет. Запустите экспорт.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -200,32 +228,30 @@
|
|||
</div>
|
||||
|
||||
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ -->
|
||||
<div class="row mt-4">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-info font-monospace">
|
||||
[FEED] СВЕЖИЕ_ПОСТУПЛЕНИЯ_В_АРХИВ (ПОСЛЕДНИЕ 10)
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ (ПОСЛЕДНИЕ 10)</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0" style="font-size: 0.85rem;">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 180px;">Дата</th>
|
||||
<th style="width: 200px;">Ведомство</th>
|
||||
<th style="width: 170px;">Дата</th>
|
||||
<th style="width: 220px;">Ведомство</th>
|
||||
<th>Тема письма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for email in latest_emails %}
|
||||
<tr>
|
||||
<td><code class="text-info font-monospace">{{ email.date }}</code></td>
|
||||
<td><span class="badge bg-secondary font-monospace">{{ email.domain }}</span></td>
|
||||
<td class="text-truncate text-white" style="max-width: 500px;">{{ email.subject }}</td>
|
||||
<td><code>{{ email.date }}</code></td>
|
||||
<td><span class="badge-status">{{ email.domain }}</span></td>
|
||||
<td class="text-truncate" style="max-width: 550px;">{{ email.subject }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center opacity-50 py-3">Новых файлов пока нет</td>
|
||||
<td colspan="3" class="text-center text-muted py-3">Новых файлов пока нет</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -270,7 +296,6 @@ async function checkPipelineStatus() {
|
|||
|
||||
if (data.last_sync_time && data.last_sync_time !== '-') {
|
||||
if (widgetLastSync) {
|
||||
// Извлекаем только дату из YYYY-MM-DD HH:MM:SS
|
||||
const parts = data.last_sync_time.split(' ');
|
||||
if (parts[0]) {
|
||||
const dateParts = parts[0].split('-');
|
||||
|
|
@ -287,12 +312,9 @@ async function checkPipelineStatus() {
|
|||
time.textContent = data.updated_at || '-';
|
||||
|
||||
if (data.status === 'running') {
|
||||
card.className = "card border-primary mb-4 shadow";
|
||||
step.className = "text-info fw-bold";
|
||||
step.textContent = data.step;
|
||||
step.innerHTML = '<b>' + data.step + '</b>';
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
// Динамический прогресс
|
||||
let width = '10%';
|
||||
if (data.step.includes('Сбор')) width = '35%';
|
||||
else if (data.step.includes('Консолидация')) width = '70%';
|
||||
|
|
@ -300,30 +322,22 @@ async function checkPipelineStatus() {
|
|||
progress.style.width = width;
|
||||
progress.className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
|
||||
|
||||
// Опрашиваем часто при работе
|
||||
setTimeout(checkPipelineStatus, 2000);
|
||||
} else if (data.status === 'success') {
|
||||
card.className = "card border-success mb-4 shadow";
|
||||
step.className = "text-success fw-bold";
|
||||
step.innerHTML = '✅ Завершен успешно';
|
||||
step.innerHTML = '<span class="badge-status badge-success">Завершен успешно</span>';
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '100%';
|
||||
progress.className = "progress-bar bg-success";
|
||||
|
||||
// Опрашиваем реже в простое
|
||||
setTimeout(checkPipelineStatus, 15000);
|
||||
} else if (data.status === 'error' || data.status === 'stuck') {
|
||||
card.className = "card border-danger mb-4 shadow";
|
||||
step.className = "text-danger fw-bold";
|
||||
step.innerHTML = '❌ ' + (data.step || 'Ошибка выполнения');
|
||||
step.innerHTML = '<span class="badge-status badge-danger">' + (data.step || 'Ошибка') + '</span>';
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '100%';
|
||||
progress.className = "progress-bar bg-danger";
|
||||
|
||||
setTimeout(checkPipelineStatus, 15000);
|
||||
} else {
|
||||
card.className = "card border-secondary mb-4 shadow";
|
||||
step.className = "text-muted";
|
||||
step.textContent = "Неактивен";
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '0%';
|
||||
|
|
@ -351,7 +365,6 @@ document.getElementById('fullSyncBtn').addEventListener('click', async function(
|
|||
const response = await fetch('/api/emails/run_full_sync', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
// Запускаем мониторинг статуса
|
||||
setTimeout(checkPipelineStatus, 1000);
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,26 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Экспорт - Домовой Бот{% endblock %}
|
||||
{% block title %}Резервное копирование и экспорт - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_export %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4" style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">>_ ЭКСПОРТ_ДАННЫХ</h2>
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Резервное копирование и экспорт базы</h4>
|
||||
<div class="small text-muted">Формирование дампов базы данных и синхронизация с локальным Git-сервером (Forgejo)</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-header border-success">
|
||||
<i class="bi bi-file-earmark-json"></i> ЭКСПОРТ В JSON & GITEA
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-file-earmark-arrow-up"></i> ЭКСПОРТ ДАННЫХ В JSON И GIT
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Экспорт ВСЕХ пользователей и сообщений. Файл будет автоматически отправлен в Gitea.</p>
|
||||
<button class="btn btn-matrix w-100" onclick="exportJSON()">
|
||||
<i class="bi bi-cloud-arrow-up"></i> ЗАПУСТИТЬ ПОЛНЫЙ ЭКСПОРТ
|
||||
<p class="small text-muted mb-3">
|
||||
Экспорт всех учетных записей жильцов, истории сообщений и настроек. Файл формируется в формате JSON и автоматически фиксируется коммитом в Git.
|
||||
</p>
|
||||
<button class="btn-unix btn-unix-primary w-100" onclick="exportJSON()">
|
||||
<i class="bi bi-arrow-up-circle"></i> Запустить полный экспорт сейчас
|
||||
</button>
|
||||
<div id="exportResult" class="mt-3"></div>
|
||||
</div>
|
||||
|
|
@ -23,62 +28,70 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-header border-success">
|
||||
<i class="bi bi-graph-up"></i> СТАТИСТИКА ВЫГРУЗОК
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-clock-history"></i> СТАТИСТИКА ВЫГРУЗОК
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="h4 text-matrix mb-3">
|
||||
Всего выгрузок: <span class="badge bg-success">{{ export_stats.total }}</span>
|
||||
<table class="table mb-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-weight: bold; width: 180px;">Всего выгрузок:</td>
|
||||
<td><span class="badge-status badge-success">{{ export_stats.total }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Последняя выгрузка:</td>
|
||||
<td>{{ fmt_local(export_stats.last_date) if export_stats.last_date else 'Никогда' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Авторасписание:</td>
|
||||
<td>Каждое воскресенье в 03:00 (cron)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="small text-muted">
|
||||
Архивы сохраняются локально в каталог <code>exports/</code> и отправляются в репозиторий на Synology NAS.
|
||||
</div>
|
||||
<p>Последняя выгрузка:<br>
|
||||
<span class="text-success">{{ fmt_local(export_stats.last_date) if export_stats.last_date else 'Никогда' }}</span></p>
|
||||
<hr class="border-success">
|
||||
<p class="text-muted small">
|
||||
Автоматический экспорт выполняется раз в неделю (вс, 03:00) и пушится в репозиторий.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script>
|
||||
async function exportJSON() {
|
||||
const resultDiv = document.getElementById('exportResult');
|
||||
const btn = document.querySelector('button[onclick="exportJSON()"]');
|
||||
async function exportJSON() {
|
||||
const resultDiv = document.getElementById('exportResult');
|
||||
const btn = document.querySelector('button[onclick="exportJSON()"]');
|
||||
|
||||
resultDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border spinner-border-sm me-2"></div>Сбор данных и отправка в Gitea...</div>';
|
||||
btn.disabled = true;
|
||||
resultDiv.innerHTML = '<div class="alert alert-info py-2 small"><div class="spinner-border spinner-border-sm me-2"></div>Сбор данных и отправка в Git...</div>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/export/json');
|
||||
const data = await response.json();
|
||||
try {
|
||||
const response = await fetch('/api/export/json');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
✅ <b>Успех!</b><br>
|
||||
Файл: <code>${data.filename}</code><br>
|
||||
Сообщений: ${data.messages}<br>
|
||||
Статус Git: ${data.git ? '🚀 Отправлено в Gitea' : '❌ Ошибка Git'}<br>
|
||||
<br>
|
||||
<a href="/exports/${data.filename}" class="btn btn-outline-success btn-sm w-100 mb-2">
|
||||
<i class="bi bi-download"></i> СКАЧАТЬ ФАЙЛ ЛОКАЛЬНО
|
||||
if (response.ok && data.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success py-2 small">
|
||||
<b>Успешно!</b><br>
|
||||
Файл: <code>${data.filename}</code><br>
|
||||
Сообщений: ${data.messages}<br>
|
||||
Статус Git: ${data.git ? 'Синхронизировано в Forgejo' : 'Ошибка Git'}<br>
|
||||
<div class="mt-2">
|
||||
<a href="/exports/${data.filename}" class="btn-unix">
|
||||
Скачать файл локально
|
||||
</a>
|
||||
<small>Страница обновится через 10 сек...</small>
|
||||
</div>
|
||||
`;
|
||||
setTimeout(() => location.reload(), 10000);
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${data.error || 'Неизвестно'}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка сети: ${error.message}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
</div>
|
||||
`;
|
||||
setTimeout(() => location.reload(), 10000);
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger py-2 small">Ошибка: ${data.error || 'Неизвестная ошибка'}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger py-2 small">Сетевая ошибка: ${error.message}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Менеджер артефактов - LKM37{% endblock %}
|
||||
{% block title %}Файлы и архивы - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_files %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4" style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
||||
>_ ЦЕНТРАЛЬНЫЙ_АРХИВ_ДАННЫХ (FILES_CORE)
|
||||
</h2>
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Менеджер файлов и медиа-артефактов</h4>
|
||||
<div class="small text-muted">Хранилище документов, системных архивов и медиа-файлов для рассылок</div>
|
||||
</div>
|
||||
|
||||
<!-- Секция загрузки артефактов -->
|
||||
<div class="row mb-4">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="card bg-black border-success shadow-sm">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-cloud-upload"></i> ЗАГРУЗКА МЕДИА-ФАЙЛА (РЕГИСТРАЦИЯ В TELEGRAM)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="text-success mb-3"><i class="bi bi-cloud-upload"></i> РЕГИСТРАЦИЯ НОВОГО АРТЕФАКТА В TELEGRAM</h5>
|
||||
<p class="small text-muted mb-3">Загрузите файл сюда, и бот отправит его вам в личку, чтобы получить постоянный <code>file_id</code>. Вы сможете использовать этот ID в рассылках и постах многократно.</p>
|
||||
<form id="uploadForm" class="row g-3">
|
||||
<p class="small text-muted mb-3">
|
||||
Загрузите изображение или документ. Бот сохранит его в Telegram и сгенерирует постоянный <code>file_id</code> для безопасного использования в постах и рассылках.
|
||||
</p>
|
||||
<form id="uploadForm" class="row g-2 align-items-center">
|
||||
<div class="col-md-9">
|
||||
<input type="file" id="fileInput" class="form-control bg-dark border-success text-success">
|
||||
<input type="file" id="fileInput" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="button" id="uploadBtn" onclick="uploadFile()" class="btn btn-matrix w-100">
|
||||
<button type="button" id="uploadBtn" onclick="uploadFile()" class="btn-unix btn-unix-primary w-100">
|
||||
<span id="uploadSpinner" class="spinner-border spinner-border-sm d-none" role="status"></span>
|
||||
ЗАГРУЗИТЬ
|
||||
Загрузить в бота
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -32,26 +37,26 @@
|
|||
</div>
|
||||
|
||||
<!-- Таблица артефактов -->
|
||||
<div class="card mb-4 border-info">
|
||||
<div class="card-header border-info bg-info bg-opacity-10 d-flex justify-content-between align-items-center">
|
||||
<span class="text-info fw-bold">💠 АРТЕФАКТЫ TELEGRAM (ДЛЯ РАССЫЛОК)</span>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="loadFiles()">
|
||||
<i class="bi bi-arrow-clockwise"></i> ОБНОВИТЬ
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>ЗАРЕГИСТРИРОВАННЫЕ МЕДИА-АРТЕФАКТЫ TELEGRAM</span>
|
||||
<button class="btn-unix" onclick="loadFiles()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Обновить список
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr class="text-info opacity-75" style="font-size: 0.8em;">
|
||||
<th>ИМЯ ФАЙЛА</th>
|
||||
<th>ТИП</th>
|
||||
<th>TELEGRAM_FILE_ID</th>
|
||||
<th class="text-end">ДЕЙСТВИЯ</th>
|
||||
<tr>
|
||||
<th>Имя файла</th>
|
||||
<th style="width: 120px;">Тип</th>
|
||||
<th>Telegram File ID</th>
|
||||
<th class="text-end" style="width: 120px;">Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="artifactsTable">
|
||||
<!-- Заполняется через API -->
|
||||
<!-- Заполняется через JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -59,31 +64,27 @@
|
|||
</div>
|
||||
|
||||
<!-- Таблица системных файлов -->
|
||||
<div class="card border-secondary">
|
||||
<div class="card-header border-secondary bg-secondary bg-opacity-10">
|
||||
<span class="text-secondary fw-bold">📁 СИСТЕМНЫЕ ФАЙЛЫ (ЛОГИ И ЭКСПОРТЫ)</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">СИСТЕМНЫЕ АРХИВЫ И ЛОГИ СЕРВЕРА</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr class="text-muted" style="font-size: 0.8em;">
|
||||
<th>ИМЯ ФАЙЛА</th>
|
||||
<th>КАТЕГОРИЯ</th>
|
||||
<th>РАЗМЕР</th>
|
||||
<th class="text-end">ДЕЙСТВИЯ</th>
|
||||
<tr>
|
||||
<th>Имя файла</th>
|
||||
<th style="width: 140px;">Категория</th>
|
||||
<th style="width: 120px;">Размер</th>
|
||||
<th class="text-end" style="width: 100px;">Скачать</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="systemFilesTable">
|
||||
<!-- Заполняется через API -->
|
||||
<!-- Заполняется через JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', loadFiles);
|
||||
|
||||
|
|
@ -109,12 +110,12 @@ async function loadFiles() {
|
|||
artifacts.forEach(f => {
|
||||
artifactsTable.innerHTML += `
|
||||
<tr>
|
||||
<td class="font-monospace text-info">${f.filename}</td>
|
||||
<td><span class="badge bg-info text-dark">${f.file_type}</span></td>
|
||||
<td><code class="text-success small" style="word-break: break-all;">${f.file_id}</code></td>
|
||||
<td><b>${f.filename}</b></td>
|
||||
<td><span class="badge-status">${f.file_type}</span></td>
|
||||
<td><code style="word-break: break-all;">${f.file_id}</code></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-info" onclick="copyId('${f.file_id}')">
|
||||
<i class="bi bi-copy"></i> ID
|
||||
<button class="btn-unix" onclick="copyId('${f.file_id}')">
|
||||
Копировать ID
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -128,12 +129,12 @@ async function loadFiles() {
|
|||
systemFiles.forEach(f => {
|
||||
systemFilesTable.innerHTML += `
|
||||
<tr>
|
||||
<td class="font-monospace">${f.filename}</td>
|
||||
<td><span class="badge bg-secondary">${f.file_type}</span></td>
|
||||
<td class="text-muted">${f.size}</td>
|
||||
<td><b>${f.filename}</b></td>
|
||||
<td><span class="badge-status">${f.file_type}</span></td>
|
||||
<td>${f.size}</td>
|
||||
<td class="text-end">
|
||||
<a href="${f.path}" class="btn btn-sm btn-outline-secondary" download>
|
||||
<i class="bi bi-download"></i>
|
||||
<a href="${f.path}" class="btn-unix" download>
|
||||
Скачать
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -147,12 +148,7 @@ async function loadFiles() {
|
|||
|
||||
function copyId(id) {
|
||||
navigator.clipboard.writeText(id);
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'position-fixed bottom-0 end-0 p-3';
|
||||
toast.style.zIndex = '11';
|
||||
toast.innerHTML = `<div class="alert alert-success">FILE_ID скопирован!</div>`;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2000);
|
||||
alert('File ID скопирован в буфер обмена');
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
|
|
@ -176,7 +172,7 @@ async function uploadFile() {
|
|||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Файл успешно зарегистрирован в Telegram!\nFILE_ID: ' + result.file_id);
|
||||
alert('Файл успешно зарегистрирован в Telegram!\nFile ID: ' + result.file_id);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при регистрации: ' + result.error);
|
||||
|
|
|
|||
|
|
@ -1,102 +1,112 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Инфраструктура - Домовой Бот{% endblock %}
|
||||
{% block title %}Инфраструктура - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_infra %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4"><i class="bi bi-cpu"></i> Мониторинг Инфраструктуры</h2>
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Мониторинг инфраструктуры и сервисов</h4>
|
||||
<div class="small text-muted">Состояние сетевых узлов, замеры интернет-канала и статус фоновых пайплайнов</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="card bg-dark border-success">
|
||||
<div class="card-header border-success text-success">
|
||||
<i class="bi bi-speedometer2"></i> ТЕСТ СКОРОСТИ ИНТЕРНЕТА (150 Mbps План)
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>ТЕСТЫ СКОРОСТИ ИНТЕРНЕТ-КАНАЛА (ТАРИФ 150 МБИТ/С)</span>
|
||||
<button id="runSpeedtestBtn" class="btn-unix">
|
||||
<i class="bi bi-play-fill"></i> Запустить замер сейчас
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-dark table-hover small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Дата и Время</th>
|
||||
<th>Download (Mbps)</th>
|
||||
<th>Upload (Mbps)</th>
|
||||
<th>Ping (ms)</th>
|
||||
<th>Сервер</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Дата и время</th>
|
||||
<th>Download (Мбит/с)</th>
|
||||
<th>Upload (Мбит/с)</th>
|
||||
<th>Ping (мс)</th>
|
||||
<th>Сервер замера</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for test in speedtests %}
|
||||
<tr>
|
||||
<td>{{ test.timestamp }}</td>
|
||||
<td><b>{{ test.download }}</b></td>
|
||||
<td><b>{{ test.upload }}</b></td>
|
||||
<td>{{ test.ping }}</td>
|
||||
<td class="text-muted">{{ test.server_name }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-3">Данных замеров пока нет</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">СЕТЕВЫЕ УЗЛЫ И ХОСТЫ</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table mb-0">
|
||||
<tbody>
|
||||
{% for test in speedtests %}
|
||||
<tr>
|
||||
<td>{{ test.timestamp }}</td>
|
||||
<td class="text-info">{{ test.download }}</td>
|
||||
<td class="text-warning">{{ test.upload }}</td>
|
||||
<td>{{ test.ping }}</td>
|
||||
<td class="opacity-50">{{ test.server_name }}</td>
|
||||
<td style="width: 140px;"><b>NanoPi R3S</b></td>
|
||||
<td>192.168.10.1 (Роутер / Шлюз)</td>
|
||||
<td class="text-end"><span class="badge-status badge-success">ONLINE</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Synology NAS</b></td>
|
||||
<td>192.168.10.105 (Хранилище / PostgreSQL)</td>
|
||||
<td class="text-end"><span class="badge-status badge-success">ONLINE</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>DockerServer</b></td>
|
||||
<td>192.168.10.116 (Domovoy / Maya Bot)</td>
|
||||
<td class="text-end"><span class="badge-status badge-success">ONLINE</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark border-info">
|
||||
<div class="card-header text-info">УЗЛЫ СЕТИ</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush bg-transparent">
|
||||
<li class="list-group-item bg-transparent text-white border-secondary">
|
||||
<span class="badge bg-success">ONLINE</span> NanoPi R3S (192.168.10.1)
|
||||
</li>
|
||||
<li class="list-group-item bg-transparent text-white border-secondary">
|
||||
<span class="badge bg-success">ONLINE</span> Synology NAS (192.168.10.105)
|
||||
</li>
|
||||
<li class="list-group-item bg-transparent text-white border-secondary">
|
||||
<span class="badge bg-success">ONLINE</span> DockerServer (192.168.10.116)
|
||||
</li>
|
||||
</ul>
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>ГИС ЖКХ & СБОРКА АРХИВА</span>
|
||||
<span id="gisStatusBadge" class="badge-status">СТАТУС: ПРОВЕРКА</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark border-primary">
|
||||
<div class="card-header text-primary">ГИС ЖКХ & ЮРИДИЧЕСКИЙ АРХИВ</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<button id="runGisHarvestBtn" class="btn btn-outline-primary flex-grow-1 me-2">
|
||||
<i class="bi bi-cloud-download"></i> ЗАПУСТИТЬ ЖАТВУ И PDF-СБОРКУ
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<button id="runGisHarvestBtn" class="btn-unix btn-unix-primary flex-grow-1">
|
||||
<i class="bi bi-cloud-download"></i> Запустить сборку ГИС ЖКХ
|
||||
</button>
|
||||
<button id="refreshGisStatusBtn" class="btn-unix">
|
||||
Обновить статус
|
||||
</button>
|
||||
<div id="gisStatusBadge" class="badge bg-secondary p-2">СТАТУС: НЕИЗВЕСТНО</div>
|
||||
</div>
|
||||
<div class="mb-2 d-flex justify-content-between">
|
||||
<span class="small text-muted">ПОСЛЕДНИЕ ЛОГИ ЖНЕЦА:</span>
|
||||
<button id="refreshGisStatusBtn" class="btn btn-sm btn-link p-0 text-primary">ОБНОВИТЬ СТАТУС</button>
|
||||
</div>
|
||||
<pre id="gisLogs" class="bg-black text-success p-2 small border border-secondary" style="height: 200px; overflow-y: scroll; font-size: 0.7rem;"></pre>
|
||||
<div class="small opacity-50 mt-2">Выполняет: Выкачка (Overwrite) -> Теггирование -> Склейка -> Google Drive</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark border-warning">
|
||||
<div class="card-header text-warning">ДЕЙСТВИЯ</div>
|
||||
<div class="card-body">
|
||||
<button id="runSpeedtestBtn" class="btn btn-outline-warning w-100 mb-2">
|
||||
<i class="bi bi-play-fill"></i> ЗАПУСТИТЬ ТЕСТ СКОРОСТИ СЕЙЧАС
|
||||
</button>
|
||||
<div class="small text-muted mb-1">Лог выполнения процесса:</div>
|
||||
<pre id="gisLogs" style="height: 160px; overflow-y: scroll; background-color: #ffffff; border: 1px solid var(--border-color); font-size: 11px;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script>
|
||||
document.getElementById('runSpeedtestBtn').addEventListener('click', async function() {
|
||||
const btn = this;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> ТЕСТИРУЮ (60с)...';
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Замер (до 60 сек)...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/infra/run_speedtest', { method: 'POST' });
|
||||
|
|
@ -107,16 +117,16 @@ document.getElementById('runSpeedtestBtn').addEventListener('click', async funct
|
|||
alert('Ошибка: ' + e);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill"></i> ЗАПУСТИТЬ ТЕСТ СКОРОСТИ СЕЙЧАС';
|
||||
btn.innerHTML = '<i class="bi bi-play-fill"></i> Запустить замер сейчас';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('runGisHarvestBtn').addEventListener('click', async function() {
|
||||
const btn = this;
|
||||
if (!confirm('Запустить тотальную перекачку ГИС ЖКХ и сборку PDF? Это займет значительное время.')) return;
|
||||
if (!confirm('Запустить сборку ГИС ЖКХ? Это займет некоторое время.')) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> ЖАТВА В ПРОЦЕССЕ...';
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Процесс запущен...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/infra/run_gis_harvest', { method: 'POST' });
|
||||
|
|
@ -126,7 +136,7 @@ document.getElementById('runGisHarvestBtn').addEventListener('click', async func
|
|||
alert('Ошибка: ' + e);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-cloud-download"></i> ЗАПУСТИТЬ ЖАТВУ И PDF-СБОРКУ';
|
||||
btn.innerHTML = '<i class="bi bi-cloud-download"></i> Запустить сборку ГИС ЖКХ';
|
||||
refreshGisStatus();
|
||||
}
|
||||
});
|
||||
|
|
@ -140,24 +150,23 @@ async function refreshGisStatus() {
|
|||
const data = await response.json();
|
||||
|
||||
if (data.alive) {
|
||||
badge.className = 'badge bg-success p-2';
|
||||
badge.innerText = 'СТАТУС: РАБОТАЕТ';
|
||||
badge.className = 'badge-status badge-success';
|
||||
badge.innerText = 'РАБОТАЕТ';
|
||||
} else {
|
||||
badge.className = 'badge bg-danger p-2';
|
||||
badge.innerText = 'СТАТУС: ОСТАНОВЛЕН';
|
||||
badge.className = 'badge-status';
|
||||
badge.innerText = 'ОЖИДАНИЕ';
|
||||
}
|
||||
|
||||
logs.innerText = data.log;
|
||||
logs.innerText = data.log || 'Лог чист';
|
||||
logs.scrollTop = logs.scrollHeight;
|
||||
} catch (e) {
|
||||
badge.className = 'badge bg-warning p-2';
|
||||
badge.innerText = 'ОШИБКА API';
|
||||
badge.className = 'badge-status badge-danger';
|
||||
badge.innerText = 'ОШИБКА';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('refreshGisStatusBtn').addEventListener('click', refreshGisStatus);
|
||||
window.addEventListener('load', refreshGisStatus);
|
||||
// Авто-обновление раз в 30 секунд
|
||||
setInterval(refreshGisStatus, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,138 +1,138 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Телефоны и службы - Домовой Бот{% endblock %}
|
||||
|
||||
{% block title %}Телефоны и службы - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_phones %}active{% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
.service-image-preview { max-width: 200px; max-height: 200px; object-fit: contain; }
|
||||
.service-image-preview { max-width: 120px; max-height: 80px; object-fit: contain; border: 1px solid var(--border-light); }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4" style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">>_ ТЕЛЕФОНЫ_И_СЛУЖБЫ</h2>
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Телефоны экстренных и обслуживающих служб</h4>
|
||||
<div class="small text-muted">Справочник полезных контактов, аварийных служб и мастеров для жителей дома</div>
|
||||
</div>
|
||||
|
||||
<!-- Добавить службу -->
|
||||
<div class="card mb-4 border-success">
|
||||
<div class="card-header border-success">
|
||||
<h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить службу</h5>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle"></i> ДОБАВЛЕНИЕ НОВОЙ СЛУЖБЫ
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="addServiceForm" class="row g-3">
|
||||
<form id="addServiceForm" class="row g-2 align-items-center">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control bg-black border-success text-success" id="serviceName" placeholder="Название" required>
|
||||
<input type="text" class="form-control" id="serviceName" placeholder="Название службы" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control bg-black border-success text-success" id="servicePhone" placeholder="Телефон" required>
|
||||
<input type="text" class="form-control" id="servicePhone" placeholder="Телефон" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select bg-black border-success text-success" id="serviceCategory">
|
||||
<option value="emergency">🚨 Экстренные</option>
|
||||
<option value="utility">🔧 ЖКХ</option>
|
||||
<option value="masters">👷 Мастера</option>
|
||||
<option value="police">👮 Полиция</option>
|
||||
<option value="other">📋 Другое</option>
|
||||
<select class="form-select" id="serviceCategory">
|
||||
<option value="emergency">Экстренные службы</option>
|
||||
<option value="utility" selected>ЖКХ и аварийные</option>
|
||||
<option value="masters">Мастера и ремонт</option>
|
||||
<option value="police">Полиция и охрана</option>
|
||||
<option value="other">Прочие контакты</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control bg-black border-success text-success" id="serviceDesc" placeholder="Описание">
|
||||
<input type="text" class="form-control" id="serviceDesc" placeholder="Описание / часы работы">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-matrix w-100">Добавить</button>
|
||||
<button type="submit" class="btn-unix btn-unix-primary w-100">Добавить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Загрузить картинку -->
|
||||
<div class="card mb-4 border-success">
|
||||
<div class="card-header border-success">
|
||||
<h5 class="mb-0"><i class="bi bi-image"></i> Загрузить картинку для службы</h5>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-image"></i> ЗАГРУЗКА ИЗОБРАЖЕНИЯ ДЛЯ КАТЕГОРИИ
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="uploadImageForm" class="row g-3" enctype="multipart/form-data">
|
||||
<form id="uploadImageForm" class="row g-2 align-items-center" enctype="multipart/form-data">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select bg-black border-success text-success" id="imageCategory" required>
|
||||
<option value="emergency">🚨 Экстренные</option>
|
||||
<option value="utility">🔧 ЖКХ</option>
|
||||
<option value="masters">👷 Мастера</option>
|
||||
<option value="police">👮 Полиция</option>
|
||||
<option value="other">📋 Другое</option>
|
||||
<select class="form-select" id="imageCategory" required>
|
||||
<option value="emergency">Экстренные службы</option>
|
||||
<option value="utility" selected>ЖКХ и аварийные</option>
|
||||
<option value="masters">Мастера и ремонт</option>
|
||||
<option value="police">Полиция и охрана</option>
|
||||
<option value="other">Прочие контакты</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" class="form-control bg-black border-success text-success" id="imageFile" accept="image/*" required>
|
||||
<small class="text-muted">JPG, PNG до 5MB</small>
|
||||
<div class="col-md-5">
|
||||
<input type="file" class="form-control" id="imageFile" accept="image/*" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-matrix w-100">Загрузить</button>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn-unix w-100">Загрузить картинку</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="uploadResult" class="mt-3"></div>
|
||||
<div id="uploadResult" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список служб -->
|
||||
<div class="card border-success">
|
||||
<div class="card-body">
|
||||
<h5 class="text-success opacity-75">СУЩЕСТВУЮЩИЕ СЛУЖБЫ:</h5>
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Категория</th>
|
||||
<th>Название</th>
|
||||
<th>Телефон</th>
|
||||
<th>Описание</th>
|
||||
<th>Картинка</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if service.category == 'emergency' %}
|
||||
<span class="badge bg-danger">🚨 Экстренные</span>
|
||||
{% elif service.category == 'utility' %}
|
||||
<span class="badge bg-info">🔧 ЖКХ</span>
|
||||
{% elif service.category == 'masters' %}
|
||||
<span class="badge bg-success">👷 Мастера</span>
|
||||
{% elif service.category == 'police' %}
|
||||
<span class="badge bg-warning text-dark">👮 Полиция</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Другое</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ service.name }}</td>
|
||||
<td><a href="tel:{{ service.phone }}">{{ service.phone }}</a></td>
|
||||
<td>{{ service.description or '—' }}</td>
|
||||
<td>
|
||||
{% if service.image_path %}
|
||||
<img src="/static/service_images/{{ service.image_path.split('/')[-1] }}"
|
||||
class="service-image-preview" alt="Картинка">
|
||||
<br><small class="text-muted">✅</small>
|
||||
{% else %}
|
||||
<span class="text-muted">❌ Нет</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteService({{ service.service_id }})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not services %}
|
||||
<p class="text-center text-muted">Служб нет</p>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<div class="card-header">СПИСОК КОНТАКТОВ В БАЗЕ ДАННЫХ</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 150px;">Категория</th>
|
||||
<th>Название</th>
|
||||
<th style="width: 160px;">Телефон</th>
|
||||
<th>Описание</th>
|
||||
<th style="width: 130px;">Изображение</th>
|
||||
<th class="text-end" style="width: 90px;">Удалить</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if service.category == 'emergency' %}
|
||||
<span class="badge-status badge-danger">Экстренные</span>
|
||||
{% elif service.category == 'utility' %}
|
||||
<span class="badge-status badge-warning">ЖКХ</span>
|
||||
{% elif service.category == 'masters' %}
|
||||
<span class="badge-status badge-success">Мастера</span>
|
||||
{% elif service.category == 'police' %}
|
||||
<span class="badge-status badge-info">Полиция</span>
|
||||
{% else %}
|
||||
<span class="badge-status">Другое</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><b>{{ service.name }}</b></td>
|
||||
<td><a href="tel:{{ service.phone }}"><b>{{ service.phone }}</b></a></td>
|
||||
<td>{{ service.description or '—' }}</td>
|
||||
<td>
|
||||
{% if service.image_path %}
|
||||
<img src="/static/service_images/{{ service.image_path.split('/')[-1] }}"
|
||||
class="service-image-preview" alt="Превью">
|
||||
{% else %}
|
||||
<span class="text-muted small">Нет</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button class="btn-unix" onclick="deleteService({{ service.service_id }})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-3">Службы пока не добавлены</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script>
|
||||
// Добавление службы
|
||||
document.getElementById('addServiceForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
|
|
@ -146,11 +146,10 @@
|
|||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await r.json();
|
||||
if (result.success) { alert('✅ Служба добавлена'); location.reload(); }
|
||||
else { alert('❌ Ошибка: ' + (result.error || 'Неизвестная')); }
|
||||
if (result.success) { alert('Служба добавлена'); location.reload(); }
|
||||
else { alert('Ошибка: ' + (result.error || 'Неизвестная')); }
|
||||
});
|
||||
|
||||
// Загрузка картинки
|
||||
document.getElementById('uploadImageForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData();
|
||||
|
|
@ -158,7 +157,7 @@
|
|||
formData.append('file', document.getElementById('imageFile').files[0]);
|
||||
|
||||
const resultDiv = document.getElementById('uploadResult');
|
||||
resultDiv.innerHTML = '<div class="alert alert-info">Загрузка...</div>';
|
||||
resultDiv.innerHTML = '<div class="alert alert-info py-1 small">Загрузка...</div>';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/service/upload_image', {
|
||||
|
|
@ -168,20 +167,21 @@
|
|||
const result = await r.json();
|
||||
|
||||
if (result.success) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}<br>Файл: ${result.file_path}</div>`;
|
||||
resultDiv.innerHTML = `<div class="alert alert-success py-1 small">${result.message}</div>`;
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error}</div>`;
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger py-1 small">Ошибка: ${result.error}</div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger py-1 small">Ошибка: ${err.message}</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteService(id) {
|
||||
if (!confirm('Удалить службу?')) return;
|
||||
if (!confirm('Удалить эту службу?')) return;
|
||||
const r = await fetch(`/api/service/${id}/delete`, { method: 'POST' });
|
||||
const result = await r.json();
|
||||
if (result.success) { alert('✅ Удалено'); location.reload(); }
|
||||
if (result.success) { alert('Удалено'); location.reload(); }
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-info">🖼️ Готовое фото (Артефакт):</label>
|
||||
<select class="form-select bg-dark text-info border-info" id="artifactPhotoId">
|
||||
<label class="form-label">🖼️ Готовое фото (Артефакт):</label>
|
||||
<select class="form-select" id="artifactPhotoId">
|
||||
<option value="">-- Не выбрано --</option>
|
||||
{% for a in artifacts if a.file_type == 'photo' %}
|
||||
<option value="{{ a.file_id }}">{{ a.filename }}</option>
|
||||
|
|
@ -39,8 +39,8 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-warning">📄 Готовый документ (Артефакт):</label>
|
||||
<select class="form-select bg-dark text-warning border-warning" id="artifactDocumentId">
|
||||
<label class="form-label">📄 Готовый документ (Артефакт):</label>
|
||||
<select class="form-select" id="artifactDocumentId">
|
||||
<option value="">-- Не выбрано --</option>
|
||||
{% for a in artifacts if a.file_type == 'document' %}
|
||||
<option value="{{ a.file_id }}">{{ a.filename }}</option>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,50 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Жатва ГАС Правосудие - LKM37{% endblock %}
|
||||
{% block title %}ГАС Правосудие - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_sudrf %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4"><i class="bi bi-bank text-danger"></i> Жатва ГАС Правосудие</h2>
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Сбор материалов судебных дел (ГАС «Правосудие»)</h4>
|
||||
<div class="small text-muted">Автоматизированная выгрузка карточек дел, определений и решений для базы NotebookLM</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row g-3">
|
||||
<!-- Инструкция и Статус -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card bg-dark border-danger h-100">
|
||||
<div class="card-header border-danger text-danger">
|
||||
<i class="bi bi-info-circle"></i> ИНСТРУКЦИЯ И УПРАВЛЕНИЕ
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
ИНСТРУКЦИЯ И УПРАВЛЕНИЕ
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<p class="text-white">Этот модуль предназначен для автоматизированного сбора поданных обращений, заявлений и ответов судов с портала ГАС «Правосудие» (ej.sudrf.ru) для последующей склейки в PDF и загрузки в Google Drive (NotebookLM).</p>
|
||||
<p class="small text-muted">
|
||||
Модуль собирает электронные документы судебных процессов по дому (ej.sudrf.ru), преобразует вложения в PDF и загружает в единый аналитический контур.
|
||||
</p>
|
||||
|
||||
<h5 class="text-danger mt-4"><i class="bi bi-1-circle"></i> Шаг 1: Подготовка сессии</h5>
|
||||
<p class="small text-muted">Нажмите кнопку «Запустить Жатву ГАС Правосудие» ниже. Система откроет скрытый браузер Chromium на виртуальном рабочем столе.</p>
|
||||
<div class="mb-3">
|
||||
<div style="font-weight: bold;">Шаг 1: Подготовка сессии</div>
|
||||
<div class="small text-muted">Нажмите кнопку «Запустить сессию» ниже. Будет открыт браузер на виртуальном рабочем столе.</div>
|
||||
</div>
|
||||
|
||||
<h5 class="text-danger mt-3"><i class="bi bi-2-circle"></i> Шаг 2: Авторизация ЕСИА</h5>
|
||||
<p class="small text-muted">Откройте VNC-браузер по ссылке справа и войдите в свой личный кабинет ГАС «Правосудие» через Госуслуги (потребуется пройти 2FA/SMS).</p>
|
||||
<div class="mb-3">
|
||||
<div style="font-weight: bold;">Шаг 2: Вход через Госуслуги (ЕСИА)</div>
|
||||
<div class="small text-muted">Перейдите по ссылке VNC и подтвердите вход по СМС в личном кабинете.</div>
|
||||
</div>
|
||||
|
||||
<h5 class="text-danger mt-3"><i class="bi bi-3-circle"></i> Шаг 3: Автоматический парсинг</h5>
|
||||
<p class="small text-muted">Как только вход будет обнаружен, скрипт перехватит сессию, соберет список дел, сделает скриншоты, скачает вложения и загрузит сводные PDF на Google Drive.</p>
|
||||
<div>
|
||||
<div style="font-weight: bold;">Шаг 3: Автоматический экспорт</div>
|
||||
<div class="small text-muted">После авторизации скрипт перехватит сессию, скачает дела и сформирует PDF.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top border-secondary">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-white">Статус парсера:</span>
|
||||
<div id="sudrfStatusBadge" class="badge bg-secondary p-2">НЕИЗВЕСТНО</div>
|
||||
<div class="mt-3 pt-3 border-top" style="border-color: var(--border-light) !important;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Статус процесса:</span>
|
||||
<div id="sudrfStatusBadge" class="badge-status">ПРОВЕРКА</div>
|
||||
</div>
|
||||
<button id="runSudrfParserBtn" class="btn btn-danger w-100 py-2">
|
||||
<i class="bi bi-play-circle-fill"></i> ЗАПУСТИТЬ ЖАТВУ ГАС ПРАВОСУДИЕ
|
||||
<button id="runSudrfParserBtn" class="btn-unix btn-unix-primary w-100 py-2">
|
||||
<i class="bi bi-play-circle"></i> Запустить сборку ГАС Правосудие
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,41 +52,36 @@
|
|||
</div>
|
||||
|
||||
<!-- VNC и Логи -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card bg-dark border-info h-100">
|
||||
<div class="card-header border-info text-info">
|
||||
<i class="bi bi-display"></i> СЕССИЯ И ЛОГИ СКАНИРОВАНИЯ
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>VNC-ЭКРАН И ЖУРНАЛ СКАНИРОВАНИЯ</span>
|
||||
<button id="refreshSudrfStatusBtn" class="btn-unix">
|
||||
<i class="bi bi-arrow-clockwise"></i> Обновить
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<a href="http://192.168.10.116:6080/vnc.html" target="_blank" class="btn btn-outline-info w-100 py-3 mb-2">
|
||||
<i class="bi bi-window-fullscreen"></i> ОТКРЫТЬ VNC ЭКРАН (noVNC)
|
||||
<a href="http://192.168.10.116:6080/vnc.html" target="_blank" class="btn-unix w-100 text-center py-2 mb-2">
|
||||
<i class="bi bi-display"></i> Открыть VNC экран (noVNC)
|
||||
</a>
|
||||
<div class="alert alert-warning py-2 mb-3 text-warning border-warning" style="font-size: 0.75rem; background: transparent;">
|
||||
<i class="bi bi-exclamation-triangle"></i> <strong>Внимание:</strong> Если жатва не запущена, VNC-экран будет пустым (черным). Браузер Chromium автоматически откроется на экране сразу после нажатия кнопки «Запустить жатву».
|
||||
<div class="small text-muted mb-2">
|
||||
<i>Экран браузера отображается в noVNC сразу после запуска сессии.</i>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="small text-muted">ЛОГИ В РЕАЛЬНОМ ВРЕМЕНИ:</span>
|
||||
<button id="refreshSudrfStatusBtn" class="btn btn-sm btn-outline-secondary py-0 px-2" style="font-size: 0.75rem;">
|
||||
<i class="bi bi-arrow-clockwise"></i> ОБНОВИТЬ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pre id="sudrfLogs" class="bg-black text-danger p-3 small border border-secondary flex-grow-1" style="min-height: 250px; overflow-y: scroll; font-family: monospace; font-size: 0.75rem;"></pre>
|
||||
<div class="small text-muted mb-1">Журнал работы:</div>
|
||||
<pre id="sudrfLogs" class="flex-grow-1 mb-0" style="min-height: 220px; overflow-y: scroll; background-color: #ffffff; border: 1px solid var(--border-color); font-size: 11px;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script>
|
||||
document.getElementById('runSudrfParserBtn').addEventListener('click', async function() {
|
||||
const btn = this;
|
||||
if (!confirm('Запустить сборку судебного архива ГАС Правосудие? Потребуется войти через Госуслуги во VNC.')) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> СЕССИЯ ЗАПУЩЕНА...';
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Сессия запускается...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sudrf/run', { method: 'POST' });
|
||||
|
|
@ -85,7 +91,7 @@ document.getElementById('runSudrfParserBtn').addEventListener('click', async fun
|
|||
alert('Ошибка при запуске: ' + e);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-circle-fill"></i> ЗАПУСТИТЬ ЖАТВУ ГАС ПРАВОСУДИЕ';
|
||||
btn.innerHTML = '<i class="bi bi-play-circle"></i> Запустить сборку ГАС Правосудие';
|
||||
refreshSudrfStatus();
|
||||
}
|
||||
});
|
||||
|
|
@ -99,25 +105,23 @@ async function refreshSudrfStatus() {
|
|||
const data = await response.json();
|
||||
|
||||
if (data.alive) {
|
||||
badge.className = 'badge bg-success p-2';
|
||||
badge.innerText = 'СТАТУС: РАБОТАЕТ';
|
||||
badge.className = 'badge-status badge-success';
|
||||
badge.innerText = 'РАБОТАЕТ';
|
||||
} else {
|
||||
badge.className = 'badge bg-danger p-2';
|
||||
badge.innerText = 'СТАТУС: ОСТАНОВЛЕН';
|
||||
badge.className = 'badge-status';
|
||||
badge.innerText = 'ОЖИДАНИЕ';
|
||||
}
|
||||
|
||||
logs.innerText = data.log || "Лог пуст или еще не создан.";
|
||||
logs.innerText = data.log || 'Лог чист';
|
||||
logs.scrollTop = logs.scrollHeight;
|
||||
} catch (e) {
|
||||
badge.className = 'badge bg-warning p-2';
|
||||
badge.className = 'badge-status badge-warning';
|
||||
badge.innerText = 'ОШИБКА API';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('refreshSudrfStatusBtn').addEventListener('click', refreshSudrfStatus);
|
||||
|
||||
window.addEventListener('load', refreshSudrfStatus);
|
||||
// Автообновление логов каждые 15 секунд
|
||||
setInterval(refreshSudrfStatus, 15000);
|
||||
setInterval(refreshSudrfStatus, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,90 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Пользователи - LKM37{% endblock %}
|
||||
{% block title %}Реестр жильцов - МКД ЛенКом 37{% endblock %}
|
||||
{% block nav_users %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4" style="color: var(--matrix-green);">
|
||||
>_ РЕЕСТР_СУБЪЕКТОВ
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Реестр жильцов и доступ к ИИ</h4>
|
||||
<div class="small text-muted">Управление верификацией и статусом доступа к юридическому модулю</div>
|
||||
</div>
|
||||
{% if current_filter %}
|
||||
<span class="badge bg-info ms-2">
|
||||
{% if current_filter == 'verified' %}✅ ВЕРИФИЦИРОВАНЫ
|
||||
{% elif current_filter == 'unverified' %}⏳ ОЖИДАНИЕ
|
||||
{% elif current_filter == 'ig' %}🔵 TEAM
|
||||
{% elif current_filter == 'active_unverified' %}🔥 АКТИВНЫ
|
||||
{% else %}ВСЕ{% endif %}
|
||||
</span>
|
||||
<a href="/users" class="btn btn-sm btn-outline-secondary ms-2 text-success border-success">СБРОС</a>
|
||||
<div>
|
||||
<span class="badge-status badge-info">
|
||||
ФИЛЬТР:
|
||||
{% if current_filter == 'verified' %}ВЕРИФИЦИРОВАНЫ
|
||||
{% elif current_filter == 'unverified' %}ОЖИДАНИЕ
|
||||
{% elif current_filter == 'ig' %}ИНИЦИАТИВНАЯ ГРУППА
|
||||
{% elif current_filter == 'active_unverified' %}АКТИВНЫЕ
|
||||
{% else %}ВСЕ{% endif %}
|
||||
</span>
|
||||
<a href="/users" class="btn-unix ms-2">Сбросить фильтр</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Статистика (ПЛОСКИЕ ПЕРЕМЕННЫЕ) -->
|
||||
<style>
|
||||
.filter-card { cursor: pointer; transition: var(--transition-smooth) !important; }
|
||||
.filter-card:hover { transform: translateY(-4px) !important; }
|
||||
.filter-card.active-filter { box-shadow: 0 0 15px currentColor !important; border-width: 2px !important; }
|
||||
</style>
|
||||
|
||||
<div class="row mb-4">
|
||||
<!-- Статистика и фильтры -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-success text-success filter-card {% if not current_filter %}active-filter{% endif %}" onclick="location.href='/users'">
|
||||
<div class="card h-100" style="cursor: pointer;" onclick="location.href='/users'">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>ВСЕГО</span>
|
||||
<i class="bi bi-people"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ total_count }}</h3>
|
||||
<small>⚡️ ВСЕГО</small>
|
||||
<div style="font-size: 22px; font-weight: bold;">{{ total_count }}</div>
|
||||
<div class="small text-muted">Все пользователи</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-success text-success filter-card {% if current_filter == 'verified' %}active-filter{% endif %}" onclick="location.href='/users?filter=verified'">
|
||||
<div class="card h-100" style="cursor: pointer;" onclick="location.href='/users?filter=verified'">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>ВЕРИФИЦИРОВАНЫ</span>
|
||||
<i class="bi bi-patch-check"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ verified_count }}</h3>
|
||||
<small>✅ ВЕРИФИЦИРОВАНЫ</small>
|
||||
<div style="font-size: 22px; font-weight: bold;">{{ verified_count }}</div>
|
||||
<div class="small text-muted">С подтверждением</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-info text-info filter-card {% if current_filter == 'ig' %}active-filter{% endif %}" onclick="location.href='/users?filter=ig'">
|
||||
<div class="card h-100" style="cursor: pointer;" onclick="location.href='/users?filter=ig'">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>СОВЕТ ДОМА / ИГ</span>
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ ig_count }}</h3>
|
||||
<small>🔵 ИНИЦИАТИВНАЯ ГРУППА</small>
|
||||
<div style="font-size: 22px; font-weight: bold;">{{ ig_count }}</div>
|
||||
<div class="small text-muted">Инициативная группа</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-warning text-warning filter-card {% if current_filter == 'active_unverified' %}active-filter{% endif %}" onclick="location.href='/users?filter=active_unverified'">
|
||||
<div class="card h-100" style="cursor: pointer;" onclick="location.href='/users?filter=active_unverified'">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span>ГОСТИ</span>
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ active_unverified }}</h3>
|
||||
<small>🔥 АКТИВНЫЕ ГОСТИ</small>
|
||||
<div style="font-size: 22px; font-weight: bold;">{{ active_unverified }}</div>
|
||||
<div class="small text-muted">Без верификации</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поиск -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body bg-black border-success">
|
||||
<form method="GET" action="/users" class="row g-3">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body p-2">
|
||||
<form method="GET" action="/users" class="row g-2 align-items-center">
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control bg-dark border-success text-success" name="search" placeholder="Поиск по ID, имени, квартире..." value="{{ search }}">
|
||||
<input type="text" class="form-control" name="search" placeholder="Поиск по ID, имени, квартире..." value="{{ search }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-matrix w-100">ПОИСК</button>
|
||||
<button type="submit" class="btn-unix btn-unix-primary w-100">Найти</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -76,20 +92,19 @@
|
|||
|
||||
<!-- Таблица -->
|
||||
<div class="card">
|
||||
<div class="card-header border-success">СПИСОК_СУБЪЕКТОВ</div>
|
||||
<div class="card-header">СПИСОК СОБСТВЕННИКОВ И ЖИТЕЛЕЙ</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr class="text-success opacity-50">
|
||||
<th>ID</th>
|
||||
<th>ИМЯ</th>
|
||||
<th>НИК</th>
|
||||
<th>КВАРТИРА</th>
|
||||
<th>СТАТУС</th>
|
||||
<th>ЮРИСТ</th>
|
||||
<th>РЕЙТИНГ</th>
|
||||
<th>ДЕЙСТВИЯ</th>
|
||||
<tr>
|
||||
<th style="width: 100px;">ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Username</th>
|
||||
<th>Квартира</th>
|
||||
<th>Статус</th>
|
||||
<th style="width: 110px;">Доступ Юрист</th>
|
||||
<th>Рейтинг</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -97,23 +112,32 @@
|
|||
<tr>
|
||||
<td><code>{{ user.user_id }}</code></td>
|
||||
<td>{{ user.first_name }} {{ user.last_name or '' }}</td>
|
||||
<td>@{{ user.username or '—' }}</td>
|
||||
<td>{{ user.apartment or '—' }}</td>
|
||||
<td>
|
||||
{% if user.verified %}<span class="text-success">V</span>{% else %}<span class="text-danger">X</span>{% endif %}
|
||||
{% if user.user_id in ig_user_ids %}<span class="text-info ms-1">IG</span>{% endif %}
|
||||
{% if user.username %}
|
||||
<a href="https://t.me/{{ user.username }}" target="_blank">@{{ user.username }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><b>{{ user.apartment or '—' }}</b></td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
{% if user.verified %}
|
||||
<span class="badge-status badge-success">Верифицирован</span>
|
||||
{% else %}
|
||||
<span class="badge-status badge-warning">Не подтвержден</span>
|
||||
{% endif %}
|
||||
{% if user.user_id in ig_user_ids %}
|
||||
<span class="badge-status badge-info ms-1">ИГ</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
{% if user.has_ai_access %}checked{% endif %}
|
||||
onclick="toggleAI({{ user.user_id }})">
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ user.rating }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-success" onclick="editUser({{ user.user_id }})"><i class="bi bi-pencil"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
|||
Loading…
Reference in a new issue