Online check_0.1
parent
dc18fea2a8
commit
0acdb5f1b8
78
app.py
78
app.py
|
|
@ -1,4 +1,4 @@
|
|||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form, Query
|
||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form, Query, WebSocket, WebSocketDisconnect
|
||||
from pydantic import BaseModel
|
||||
import sqlite3
|
||||
import datetime
|
||||
|
|
@ -10,6 +10,7 @@ import io
|
|||
import os
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from werkzeug.utils import secure_filename
|
||||
import json
|
||||
|
||||
# Создаем два экземпляра FastAPI
|
||||
web_app = FastAPI(title="Web Interface") # Для веб-интерфейса (порт 8001)
|
||||
|
|
@ -28,6 +29,9 @@ if 'protocol' not in columns:
|
|||
if 'note' not in columns:
|
||||
cursor.execute("ALTER TABLE installs ADD COLUMN note TEXT DEFAULT ''")
|
||||
conn.commit()
|
||||
if 'last_seen' not in columns:
|
||||
cursor.execute("ALTER TABLE installs ADD COLUMN last_seen TEXT DEFAULT NULL")
|
||||
conn.commit()
|
||||
|
||||
# Создаем таблицы
|
||||
cursor.execute("""
|
||||
|
|
@ -47,6 +51,7 @@ CREATE TABLE IF NOT EXISTS installs (
|
|||
folder_id INTEGER,
|
||||
protocol TEXT DEFAULT 'rustdesk',
|
||||
note TEXT DEFAULT '',
|
||||
last_seen TEXT DEFAULT NULL,
|
||||
FOREIGN KEY (folder_id) REFERENCES folders(id)
|
||||
)
|
||||
""")
|
||||
|
|
@ -110,6 +115,33 @@ web_app.mount("/icons", StaticFiles(directory="templates/icons"), name="icons")
|
|||
async def root():
|
||||
return FileResponse("templates/index.html")
|
||||
|
||||
# WebSocket для отправки обновлений статуса
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: list[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
@api_app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
# API-эндпоинты
|
||||
@api_app.get("/api/folders")
|
||||
def get_folders():
|
||||
|
|
@ -148,25 +180,44 @@ def delete_folder(folder_id: int):
|
|||
@api_app.get("/api/installs")
|
||||
def get_installs():
|
||||
cursor.execute("""
|
||||
SELECT i.id, i.rust_id, i.computer_name, i.install_time, i.folder_id, f.name as folder_name, i.protocol, i.note
|
||||
SELECT i.id, i.rust_id, i.computer_name, i.install_time, i.folder_id, f.name as folder_name, i.protocol, i.note, i.last_seen
|
||||
FROM installs i
|
||||
LEFT JOIN folders f ON i.folder_id = f.id
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
return [{"id": row[0], "rust_id": row[1], "computer_name": row[2],
|
||||
"install_time": format_time(row[3]),
|
||||
"folder_id": row[4], "folder_name": row[5], "protocol": row[6], "note": row[7]}
|
||||
"folder_id": row[4], "folder_name": row[5], "protocol": row[6], "note": row[7], "last_seen": row[8]}
|
||||
for row in rows]
|
||||
|
||||
@api_app.post("/api/install")
|
||||
def add_install(data: InstallData):
|
||||
async def add_install(data: InstallData):
|
||||
rust_id = data.rust_id
|
||||
computer_name = data.computer_name or f"PC_{rust_id}"
|
||||
install_time = data.install_time or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
folder_id = data.folder_id if data.folder_id is not None else unsorted_folder_id
|
||||
protocol = data.protocol or 'rustdesk'
|
||||
note = data.note or ''
|
||||
cursor.execute("INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol, note) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(data.rust_id, data.computer_name, install_time, folder_id, protocol, note))
|
||||
last_seen = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
cursor.execute("SELECT id FROM installs WHERE rust_id = ?", (rust_id,))
|
||||
existing = cursor.fetchone()
|
||||
if existing:
|
||||
cursor.execute("""
|
||||
UPDATE installs
|
||||
SET computer_name = ?, install_time = ?, folder_id = ?, protocol = ?, note = ?, last_seen = ?
|
||||
WHERE rust_id = ?
|
||||
""", (computer_name, install_time, folder_id, protocol, note, last_seen, rust_id))
|
||||
else:
|
||||
cursor.execute("""
|
||||
INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol, note, last_seen)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (rust_id, computer_name, install_time, folder_id, protocol, note, last_seen))
|
||||
conn.commit()
|
||||
|
||||
# Отправка обновления через WebSocket
|
||||
installs = get_installs()
|
||||
await manager.broadcast(json.dumps(installs))
|
||||
return {"status": "success"}
|
||||
|
||||
@api_app.put("/api/install/{install_id}")
|
||||
|
|
@ -180,7 +231,6 @@ def update_install(install_id: int, data: InstallData):
|
|||
new_computer_name = data.computer_name if data.computer_name is not None else current[1]
|
||||
new_install_time = data.install_time if data.install_time is not None else current[2]
|
||||
new_folder_id = data.folder_id if data.folder_id is not None else current[3]
|
||||
# Сохраняем текущее значение protocol, если новое не передано
|
||||
new_protocol = data.protocol if data.protocol is not None else current[4]
|
||||
new_note = data.note if data.note is not None else current[5]
|
||||
|
||||
|
|
@ -212,14 +262,14 @@ def delete_install(install_id: int):
|
|||
async def export_csv(folder_id: int | None = Query(None, description="ID папки для экспорта, если None - экспортировать все папки")):
|
||||
if folder_id:
|
||||
cursor.execute("""
|
||||
SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note
|
||||
SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note, i.last_seen
|
||||
FROM installs i
|
||||
LEFT JOIN folders f ON i.folder_id = f.id
|
||||
WHERE i.folder_id = ?
|
||||
""", (folder_id,))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note
|
||||
SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note, i.last_seen
|
||||
FROM installs i
|
||||
LEFT JOIN folders f ON i.folder_id = f.id
|
||||
""")
|
||||
|
|
@ -227,10 +277,11 @@ async def export_csv(folder_id: int | None = Query(None, description="ID пап
|
|||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output, lineterminator='\n')
|
||||
writer.writerow(['ID подключения', 'Имя компьютера', 'Время установки', 'Папка', 'Протокол', 'Заметка'])
|
||||
writer.writerow(['ID подключения', 'Имя компьютера', 'Время установки', 'Папка', 'Протокол', 'Заметка', 'Последнее подключение'])
|
||||
for row in rows:
|
||||
install_time = format_time(row[2]) if row[2] else ""
|
||||
writer.writerow([row[0], row[1], install_time, row[3], row[4], row[5]])
|
||||
last_seen = format_time(row[6]) if row[6] else ""
|
||||
writer.writerow([row[0], row[1], install_time, row[3], row[4], row[5], last_seen])
|
||||
|
||||
headers = {
|
||||
'Content-Disposition': 'attachment; filename="rustdesk_data.csv"',
|
||||
|
|
@ -287,26 +338,21 @@ async def import_csv(file: UploadFile = File(...), folder_id: int | None = Query
|
|||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Ошибка импорта: {str(e)}")
|
||||
|
||||
# Новый эндпоинт для загрузки изображений
|
||||
@api_app.post("/api/upload-image")
|
||||
async def upload_image(install_id: int = Form(...), file: UploadFile = File(...)):
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No file provided")
|
||||
|
||||
# Проверка расширения файла
|
||||
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
if not secure_filename(file.filename).rsplit('.', 1)[1].lower() in allowed_extensions:
|
||||
raise HTTPException(status_code=400, detail="Invalid file format. Use png, jpg, jpeg, or gif")
|
||||
|
||||
# Генерация безопасного имени файла
|
||||
filename = secure_filename(f"{install_id}_{file.filename}")
|
||||
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
|
||||
# Сохранение файла
|
||||
with open(file_path, "wb") as buffer:
|
||||
buffer.write(await file.read())
|
||||
|
||||
# Возвращаем URL изображения
|
||||
url = f"/uploads/{filename}"
|
||||
return {"url": url}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ fastapi
|
|||
uvicorn
|
||||
python-multipart
|
||||
markdown
|
||||
werkzeug
|
||||
werkzeug
|
||||
websockets
|
||||
|
|
@ -8,320 +8,10 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/themes/default/style.min.css">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
body.dark-theme {
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
#tree-container {
|
||||
width: 15%; /* Начальная ширина */
|
||||
padding: 10px;
|
||||
border-right: none; /* Убираем стандартную границу */
|
||||
min-width: 150px; /* Минимальная ширина */
|
||||
max-width: 50%; /* Максимальная ширина */
|
||||
overflow: auto; /* Для длинных списков папок */
|
||||
}
|
||||
#tree-container.dark-theme { border-right-color: #444; }
|
||||
#splitter {
|
||||
width: 5px;
|
||||
background: #ccc;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
#splitter.dark-theme {
|
||||
background: #444;
|
||||
}
|
||||
#splitter:hover {
|
||||
background: #aaa;
|
||||
}
|
||||
#splitter.dark-theme:hover {
|
||||
background: #666;
|
||||
}
|
||||
#installs-container {
|
||||
flex: 1; /* Занимает оставшееся пространство */
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
min-width: 300px; /* Минимальная ширина для центральной части */
|
||||
}
|
||||
.install-item {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2px 0;
|
||||
background: #fff;
|
||||
cursor: move;
|
||||
color: #000;
|
||||
}
|
||||
.install-item.dark-theme {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.install-item.selected { background-color: #e6f7ff; }
|
||||
.install-item.selected.dark-theme { background-color: #003a6d; }
|
||||
.install-item > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.install-item .computer-name {
|
||||
width: 20%;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.install-item .connection-id {
|
||||
width: 30%;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.install-item .install-time {
|
||||
width: 25%;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.install-item .actions { width: 25%; }
|
||||
.form-container { margin-bottom: 20px; }
|
||||
.jstree-node { position: relative; }
|
||||
.folder-actions {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: 0;
|
||||
}
|
||||
.jstree-node.highlight {
|
||||
background-color: #d4edda !important;
|
||||
border: 2px dashed #28a745;
|
||||
}
|
||||
.jstree-node.highlight.dark-theme {
|
||||
background-color: #2f4f4f !important;
|
||||
border: 2px dashed #5cb85c;
|
||||
}
|
||||
.jstree-default-dark .jstree-clicked {
|
||||
background: #005580 !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
.jstree-default-dark .jstree-hovered {
|
||||
background: #003d66 !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
#search-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px; /* Расстояние между элементами */
|
||||
}
|
||||
#search-container.dark-theme input, #search-container.dark-theme select {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
#search-input {
|
||||
width: 300px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 5px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
#search-input:focus {
|
||||
border-color: #0056b3;
|
||||
outline: none;
|
||||
}
|
||||
#search-input.dark-theme {
|
||||
border-color: #66b3ff;
|
||||
}
|
||||
#search-input.dark-theme:focus {
|
||||
border-color: #99ccff;
|
||||
}
|
||||
#folder-select {
|
||||
width: 300px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 5px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
#folder-select:focus {
|
||||
border-color: #0056b3;
|
||||
outline: none;
|
||||
}
|
||||
#folder-select.dark-theme {
|
||||
border-color: #66b3ff;
|
||||
}
|
||||
#folder-select.dark-theme:focus {
|
||||
border-color: #99ccff;
|
||||
}
|
||||
#installs-list { margin-top: 0; }
|
||||
.header {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #f0f0f0;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
}
|
||||
.header.dark-theme {
|
||||
background: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.header > div {
|
||||
display: table-cell;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.header > div:hover {
|
||||
background: #e0e0e0;
|
||||
color: #000;
|
||||
}
|
||||
.header.dark-theme > div:hover {
|
||||
background: #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.sort-arrow { font-size: 12px; margin-left: 5px; }
|
||||
.protocol-icon {
|
||||
margin-right: 5px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.connection-link {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.connection-link.dark-theme { color: #66b3ff; }
|
||||
.edit-button, .copy-button, .action-buttons button, #add-modal button, #note-modal button, .export-import-container button, #import-label, .theme-toggle {
|
||||
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.edit-button:hover, .copy-button:hover, .action-buttons button:hover, #add-modal button:hover, #note-modal button:hover, .export-import-container button:hover, #import-label:hover, .theme-toggle:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(90deg, #3d9afe 0%, #00d4fe 100%);
|
||||
}
|
||||
.edit-button.dark-theme, .copy-button.dark-theme, .action-buttons button.dark-theme, #add-modal button.dark-theme, #note-modal button.dark-theme, .export-import-container button.dark-theme, #import-label.dark-theme, .theme-toggle.dark-theme {
|
||||
background: linear-gradient(90deg, #2a6eb4 0%, #1e90ff 100%);
|
||||
}
|
||||
.edit-button.dark-theme:hover, .copy-button.dark-theme:hover, .action-buttons button.dark-theme:hover, #add-modal button.dark-theme:hover, #note-modal button.dark-theme:hover, .export-import-container button.dark-theme:hover, #import-label.dark-theme:hover, .theme-toggle.dark-theme:hover {
|
||||
background: linear-gradient(90deg, #1e5ea4 0%, #1c7ed6 100%);
|
||||
}
|
||||
#add-record-button {
|
||||
display: block;
|
||||
margin-top: 30px; /* Увеличенный отступ сверху */
|
||||
margin-left: auto; /* Сдвиг вправо */
|
||||
width: 250px;
|
||||
height: 60px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
#add-record-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(90deg, #3d9afe 0%, #00d4fe 100%);
|
||||
}
|
||||
#add-record-button.dark-theme {
|
||||
background: linear-gradient(90deg, #2a6eb4 0%, #1e90ff 100%);
|
||||
}
|
||||
#add-record-button.dark-theme:hover {
|
||||
background: linear-gradient(90deg, #1e5ea4 0%, #1c7ed6 100%);
|
||||
}
|
||||
#import-form { margin-top: 5px; }
|
||||
#import-file { display: none; }
|
||||
.time-hint { font-size: 12px; color: #666; margin-top: 5px; }
|
||||
.time-hint.dark-theme { color: #bbb; }
|
||||
#notes-panel {
|
||||
width: 25%;
|
||||
padding: 10px;
|
||||
border-left: 1px solid #ccc;
|
||||
background: #f9f9f9;
|
||||
min-height: 200px;
|
||||
margin-top: 240px;
|
||||
}
|
||||
#notes-panel.dark-theme {
|
||||
border-left-color: #444;
|
||||
background: #222;
|
||||
}
|
||||
#notes-content { margin-top: 10px; }
|
||||
#notes-content img {
|
||||
max-width: 100%; /* Ограничение ширины изображений */
|
||||
height: auto;
|
||||
}
|
||||
#add-modal, #note-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
width: 500px;
|
||||
}
|
||||
#add-modal.dark-theme, #note-modal.dark-theme {
|
||||
background: #2a2a2a;
|
||||
border-color: #444;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
#add-modal .markdown-buttons, #note-modal .markdown-buttons { margin-bottom: 10px; }
|
||||
#add-modal .markdown-buttons button, #note-modal .markdown-buttons button { margin-right: 5px; }
|
||||
#add-modal textarea, #note-modal textarea { width: 100%; height: 200px; margin-bottom: 10px; }
|
||||
#add-modal.dark-theme textarea, #note-modal.dark-theme textarea {
|
||||
background: #333;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
#add-modal .image-upload, #note-modal .image-upload {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#add-modal .image-upload input, #note-modal .image-upload input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
.header-logo { display: flex; align-items: center; }
|
||||
.header-logo img { height: 50px; margin-right: 10px; }
|
||||
/* Существующие стили остаются без изменений */
|
||||
.status-online { background-color: green; width: 10px; height: 10px; display: inline-block; border-radius: 50%; margin-left: 5px; }
|
||||
.status-offline { background-color: red; width: 10px; height: 10px; display: inline-block; border-radius: 50%; margin-left: 5px; }
|
||||
/* Остальные стили остаются без изменений */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -407,6 +97,7 @@
|
|||
let selectedInstallId = null;
|
||||
let sortField = null;
|
||||
let sortDirection = 'asc';
|
||||
let ws = null;
|
||||
|
||||
const protocolIcons = {
|
||||
'rustdesk': '<img src="/icons/rustdesk.png" class="protocol-icon">',
|
||||
|
|
@ -445,14 +136,12 @@
|
|||
$('#folder-tree').addClass('jstree-default-dark');
|
||||
}
|
||||
|
||||
// Делегирование событий для кнопок "Заметка"
|
||||
$('#installs-list').on('click', '.action-buttons button:nth-child(2)', function () {
|
||||
const installId = $(this).closest('.install-item').data('id');
|
||||
const install = allInstalls.find(i => i.id === installId);
|
||||
openNoteModal(installId, install?.note || '');
|
||||
});
|
||||
|
||||
// Логика изменения размера панели
|
||||
const splitter = document.getElementById('splitter');
|
||||
const treeContainer = document.getElementById('tree-container');
|
||||
let isResizing = false;
|
||||
|
|
@ -469,7 +158,7 @@
|
|||
const containerRect = document.body.getBoundingClientRect();
|
||||
let newWidth = e.clientX - containerRect.left;
|
||||
const minWidth = parseInt(getComputedStyle(treeContainer).minWidth, 10);
|
||||
const maxWidth = containerRect.width * 0.5; // Максимум 50% ширины окна
|
||||
const maxWidth = containerRect.width * 0.5;
|
||||
|
||||
if (newWidth < minWidth) newWidth = minWidth;
|
||||
if (newWidth > maxWidth) newWidth = maxWidth;
|
||||
|
|
@ -482,7 +171,6 @@
|
|||
document.body.style.cursor = 'default';
|
||||
});
|
||||
|
||||
// Обработка клавиш для навигации
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!selectedFolderId || !allInstalls.length) return;
|
||||
|
||||
|
|
@ -505,6 +193,9 @@
|
|||
updateNotesPanel();
|
||||
$(items[currentIndex])[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
|
||||
// Подключение к WebSocket
|
||||
connectWebSocket();
|
||||
});
|
||||
|
||||
function toggleTheme() {
|
||||
|
|
@ -665,6 +356,24 @@
|
|||
});
|
||||
});
|
||||
|
||||
function connectWebSocket() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
ws = new WebSocket(`ws://${window.location.host}/ws`);
|
||||
ws.onmessage = function(event) {
|
||||
allInstalls = JSON.parse(event.data);
|
||||
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
|
||||
};
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket disconnected, retrying in 5 seconds...');
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
function loadFolders() {
|
||||
$.getJSON(`${API_URL}/folders`, function (data) {
|
||||
allFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
|
@ -681,9 +390,7 @@
|
|||
let filtered = folderId ? data.filter(i => i.folder_id == folderId && i.folder_id !== null) : data;
|
||||
displayInstalls(filtered);
|
||||
updateNotesPanel();
|
||||
// Восстанавливаем выделение после перерендера
|
||||
if (selectedInstallId) {
|
||||
const items = $('#installs-list .install-item');
|
||||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||||
}
|
||||
}).fail(function(jqxhr, textStatus, error) {
|
||||
|
|
@ -724,17 +431,21 @@
|
|||
if (installs.length === 0) {
|
||||
$('#installs-list').html('<p>Нет результатов</p>');
|
||||
} else {
|
||||
$('#installs-list').html(installs.map(item => `
|
||||
<div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true">
|
||||
<div class="computer-name">${item.computer_name} <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'computer_name', '${item.computer_name}')">✏️</button></div>
|
||||
<div class="connection-id"><span class="protocol-icon">${protocolIcons[item.protocol]}</span><a href="${protocolLinks[item.protocol]}${item.rust_id}" class="connection-link ${isDarkTheme ? 'dark-theme' : ''}" target="_blank">${item.rust_id}</a> <button class="copy-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="copyToClipboard('${item.rust_id}')">📋</button> <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')">✏️</button></div>
|
||||
<div class="install-time">${item.install_time} <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'install_time', '${item.install_time}')">✏️</button></div>
|
||||
<div class="actions"><span class="action-buttons"><button onclick="deleteInstall(${item.id})">Удалить</button> <button class="note-button">Заметка</button></span></div>
|
||||
</div>
|
||||
`).join(''));
|
||||
$('#installs-list').html(installs.map(item => {
|
||||
const lastSeen = new Date(item.last_seen || '1970-01-01');
|
||||
const isOnline = (new Date() - lastSeen) < 60000; // Онлайн, если менее 1 минуты
|
||||
const statusClass = isOnline ? 'status-online' : 'status-offline';
|
||||
return `
|
||||
<div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true">
|
||||
<div class="computer-name">${item.computer_name} <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'computer_name', '${item.computer_name}')">✏️</button></div>
|
||||
<div class="connection-id"><span class="protocol-icon">${protocolIcons[item.protocol]}</span><a href="${protocolLinks[item.protocol]}${item.rust_id}" class="connection-link ${isDarkTheme ? 'dark-theme' : ''}" target="_blank">${item.rust_id}</a> <span class="${statusClass}"></span> <button class="copy-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="copyToClipboard('${item.rust_id}')">📋</button> <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')">✏️</button></div>
|
||||
<div class="install-time">${item.install_time} <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'install_time', '${item.install_time}')">✏️</button></div>
|
||||
<div class="actions"><span class="action-buttons"><button onclick="deleteInstall(${item.id})">Удалить</button> <button class="note-button">Заметка</button></span></div>
|
||||
</div>
|
||||
`;
|
||||
}).join(''));
|
||||
}
|
||||
updateSortArrows();
|
||||
// Восстанавливаем выделение после перерендера
|
||||
if (selectedInstallId) {
|
||||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||||
}
|
||||
|
|
@ -907,7 +618,7 @@
|
|||
const install = allInstalls.find(i => i.id === installId);
|
||||
if (install) {
|
||||
data['protocol'] = install.protocol;
|
||||
data['note'] = install.note; // Сохраняем текущую заметку
|
||||
data['note'] = install.note;
|
||||
}
|
||||
$.ajax({
|
||||
url: `${API_URL}/install/${installId}`,
|
||||
|
|
@ -947,7 +658,6 @@
|
|||
console.error('Install not found:', installId);
|
||||
return;
|
||||
}
|
||||
// Передаем все текущие данные записи, чтобы ничего не очистилось
|
||||
$.ajax({
|
||||
url: `${API_URL}/install/${installId}`,
|
||||
type: 'PUT',
|
||||
|
|
@ -958,7 +668,7 @@
|
|||
install_time: install.install_time,
|
||||
folder_id: folderId,
|
||||
protocol: install.protocol,
|
||||
note: install.note // Явно передаем заметку
|
||||
note: install.note
|
||||
}),
|
||||
success: function () {
|
||||
console.log('Move successful');
|
||||
|
|
@ -1008,7 +718,7 @@
|
|||
|
||||
function openNoteModal(installId, currentNote) {
|
||||
console.log('Opening note modal for installId:', installId, 'with note:', currentNote);
|
||||
selectedInstallId = installId; // Сохраняем выделение
|
||||
selectedInstallId = installId;
|
||||
$('#note-textarea').val(currentNote || '');
|
||||
$('#note-modal').show();
|
||||
$('#modal-overlay').show();
|
||||
|
|
@ -1063,7 +773,7 @@
|
|||
const note = $('#note-textarea').val() || '';
|
||||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||||
if (install) {
|
||||
const currentSelectedId = selectedInstallId; // Сохраняем текущий selectedInstallId
|
||||
const currentSelectedId = selectedInstallId;
|
||||
$.ajax({
|
||||
url: `${API_URL}/install/${selectedInstallId}`,
|
||||
type: 'PUT',
|
||||
|
|
@ -1078,11 +788,9 @@
|
|||
}),
|
||||
success: function () {
|
||||
console.log('Note saved successfully');
|
||||
loadInstalls(selectedFolderId); // Перезагружаем список
|
||||
selectedInstallId = currentSelectedId; // Восстанавливаем selectedInstallId
|
||||
closeNoteModal(); // Закрываем модальное окно
|
||||
// Восстанавливаем выделение
|
||||
const items = $('#installs-list .install-item');
|
||||
loadInstalls(selectedFolderId);
|
||||
selectedInstallId = currentSelectedId;
|
||||
closeNoteModal();
|
||||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||||
updateNotesPanel();
|
||||
},
|
||||
|
|
@ -1099,19 +807,12 @@
|
|||
$('#modal-overlay').hide();
|
||||
$('#note-textarea').val('');
|
||||
document.getElementById('image-upload-input').value = '';
|
||||
// Фокус остаётся на текущей записи
|
||||
if (selectedInstallId) {
|
||||
const items = $('#installs-list .install-item');
|
||||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||||
updateNotesPanel();
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddModal() {
|
||||
$('#add-modal').hide();
|
||||
$('#modal-overlay').hide();
|
||||
}
|
||||
|
||||
function updateNotesPanel() {
|
||||
if (selectedInstallId) {
|
||||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||||
|
|
|
|||
Loading…
Reference in New Issue