Online check_0.1

main
Satur@it-depot.ru 2025-03-07 10:44:01 +03:00
parent dc18fea2a8
commit 0acdb5f1b8
3 changed files with 111 additions and 363 deletions

78
app.py
View File

@ -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 from pydantic import BaseModel
import sqlite3 import sqlite3
import datetime import datetime
@ -10,6 +10,7 @@ import io
import os import os
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import json
# Создаем два экземпляра FastAPI # Создаем два экземпляра FastAPI
web_app = FastAPI(title="Web Interface") # Для веб-интерфейса (порт 8001) web_app = FastAPI(title="Web Interface") # Для веб-интерфейса (порт 8001)
@ -28,6 +29,9 @@ if 'protocol' not in columns:
if 'note' not in columns: if 'note' not in columns:
cursor.execute("ALTER TABLE installs ADD COLUMN note TEXT DEFAULT ''") cursor.execute("ALTER TABLE installs ADD COLUMN note TEXT DEFAULT ''")
conn.commit() conn.commit()
if 'last_seen' not in columns:
cursor.execute("ALTER TABLE installs ADD COLUMN last_seen TEXT DEFAULT NULL")
conn.commit()
# Создаем таблицы # Создаем таблицы
cursor.execute(""" cursor.execute("""
@ -47,6 +51,7 @@ CREATE TABLE IF NOT EXISTS installs (
folder_id INTEGER, folder_id INTEGER,
protocol TEXT DEFAULT 'rustdesk', protocol TEXT DEFAULT 'rustdesk',
note TEXT DEFAULT '', note TEXT DEFAULT '',
last_seen TEXT DEFAULT NULL,
FOREIGN KEY (folder_id) REFERENCES folders(id) FOREIGN KEY (folder_id) REFERENCES folders(id)
) )
""") """)
@ -110,6 +115,33 @@ web_app.mount("/icons", StaticFiles(directory="templates/icons"), name="icons")
async def root(): async def root():
return FileResponse("templates/index.html") 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-эндпоинты
@api_app.get("/api/folders") @api_app.get("/api/folders")
def get_folders(): def get_folders():
@ -148,25 +180,44 @@ def delete_folder(folder_id: int):
@api_app.get("/api/installs") @api_app.get("/api/installs")
def get_installs(): def get_installs():
cursor.execute(""" 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 FROM installs i
LEFT JOIN folders f ON i.folder_id = f.id LEFT JOIN folders f ON i.folder_id = f.id
""") """)
rows = cursor.fetchall() rows = cursor.fetchall()
return [{"id": row[0], "rust_id": row[1], "computer_name": row[2], return [{"id": row[0], "rust_id": row[1], "computer_name": row[2],
"install_time": format_time(row[3]), "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] for row in rows]
@api_app.post("/api/install") @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") 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 folder_id = data.folder_id if data.folder_id is not None else unsorted_folder_id
protocol = data.protocol or 'rustdesk' protocol = data.protocol or 'rustdesk'
note = data.note or '' note = data.note or ''
cursor.execute("INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol, note) VALUES (?, ?, ?, ?, ?, ?)", last_seen = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
(data.rust_id, data.computer_name, install_time, folder_id, protocol, note))
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() conn.commit()
# Отправка обновления через WebSocket
installs = get_installs()
await manager.broadcast(json.dumps(installs))
return {"status": "success"} return {"status": "success"}
@api_app.put("/api/install/{install_id}") @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_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_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] 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_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] 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 - экспортировать все папки")): async def export_csv(folder_id: int | None = Query(None, description="ID папки для экспорта, если None - экспортировать все папки")):
if folder_id: if folder_id:
cursor.execute(""" 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 FROM installs i
LEFT JOIN folders f ON i.folder_id = f.id LEFT JOIN folders f ON i.folder_id = f.id
WHERE i.folder_id = ? WHERE i.folder_id = ?
""", (folder_id,)) """, (folder_id,))
else: else:
cursor.execute(""" 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 FROM installs i
LEFT JOIN folders f ON i.folder_id = f.id 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() output = io.StringIO()
writer = csv.writer(output, lineterminator='\n') writer = csv.writer(output, lineterminator='\n')
writer.writerow(['ID подключения', 'Имя компьютера', 'Время установки', 'Папка', 'Протокол', 'Заметка']) writer.writerow(['ID подключения', 'Имя компьютера', 'Время установки', 'Папка', 'Протокол', 'Заметка', 'Последнее подключение'])
for row in rows: for row in rows:
install_time = format_time(row[2]) if row[2] else "" 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 = { headers = {
'Content-Disposition': 'attachment; filename="rustdesk_data.csv"', '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: except Exception as e:
raise HTTPException(status_code=400, detail=f"Ошибка импорта: {str(e)}") raise HTTPException(status_code=400, detail=f"Ошибка импорта: {str(e)}")
# Новый эндпоинт для загрузки изображений
@api_app.post("/api/upload-image") @api_app.post("/api/upload-image")
async def upload_image(install_id: int = Form(...), file: UploadFile = File(...)): async def upload_image(install_id: int = Form(...), file: UploadFile = File(...)):
if not file.filename: if not file.filename:
raise HTTPException(status_code=400, detail="No file provided") raise HTTPException(status_code=400, detail="No file provided")
# Проверка расширения файла
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif'} allowed_extensions = {'png', 'jpg', 'jpeg', 'gif'}
if not secure_filename(file.filename).rsplit('.', 1)[1].lower() in allowed_extensions: 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") raise HTTPException(status_code=400, detail="Invalid file format. Use png, jpg, jpeg, or gif")
# Генерация безопасного имени файла
filename = secure_filename(f"{install_id}_{file.filename}") filename = secure_filename(f"{install_id}_{file.filename}")
file_path = os.path.join(UPLOAD_FOLDER, filename) file_path = os.path.join(UPLOAD_FOLDER, filename)
# Сохранение файла
with open(file_path, "wb") as buffer: with open(file_path, "wb") as buffer:
buffer.write(await file.read()) buffer.write(await file.read())
# Возвращаем URL изображения
url = f"/uploads/{filename}" url = f"/uploads/{filename}"
return {"url": url} return {"url": url}

View File

@ -2,4 +2,5 @@ fastapi
uvicorn uvicorn
python-multipart python-multipart
markdown markdown
werkzeug werkzeug
websockets

View File

@ -8,320 +8,10 @@
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <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"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/themes/default/style.min.css">
<style> <style>
body { /* Существующие стили остаются без изменений */
display: flex; .status-online { background-color: green; width: 10px; height: 10px; display: inline-block; border-radius: 50%; margin-left: 5px; }
font-family: Arial, sans-serif; .status-offline { background-color: red; width: 10px; height: 10px; display: inline-block; border-radius: 50%; margin-left: 5px; }
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; }
</style> </style>
</head> </head>
<body> <body>
@ -407,6 +97,7 @@
let selectedInstallId = null; let selectedInstallId = null;
let sortField = null; let sortField = null;
let sortDirection = 'asc'; let sortDirection = 'asc';
let ws = null;
const protocolIcons = { const protocolIcons = {
'rustdesk': '<img src="/icons/rustdesk.png" class="protocol-icon">', 'rustdesk': '<img src="/icons/rustdesk.png" class="protocol-icon">',
@ -445,14 +136,12 @@
$('#folder-tree').addClass('jstree-default-dark'); $('#folder-tree').addClass('jstree-default-dark');
} }
// Делегирование событий для кнопок "Заметка"
$('#installs-list').on('click', '.action-buttons button:nth-child(2)', function () { $('#installs-list').on('click', '.action-buttons button:nth-child(2)', function () {
const installId = $(this).closest('.install-item').data('id'); const installId = $(this).closest('.install-item').data('id');
const install = allInstalls.find(i => i.id === installId); const install = allInstalls.find(i => i.id === installId);
openNoteModal(installId, install?.note || ''); openNoteModal(installId, install?.note || '');
}); });
// Логика изменения размера панели
const splitter = document.getElementById('splitter'); const splitter = document.getElementById('splitter');
const treeContainer = document.getElementById('tree-container'); const treeContainer = document.getElementById('tree-container');
let isResizing = false; let isResizing = false;
@ -469,7 +158,7 @@
const containerRect = document.body.getBoundingClientRect(); const containerRect = document.body.getBoundingClientRect();
let newWidth = e.clientX - containerRect.left; let newWidth = e.clientX - containerRect.left;
const minWidth = parseInt(getComputedStyle(treeContainer).minWidth, 10); 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 < minWidth) newWidth = minWidth;
if (newWidth > maxWidth) newWidth = maxWidth; if (newWidth > maxWidth) newWidth = maxWidth;
@ -482,7 +171,6 @@
document.body.style.cursor = 'default'; document.body.style.cursor = 'default';
}); });
// Обработка клавиш для навигации
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (!selectedFolderId || !allInstalls.length) return; if (!selectedFolderId || !allInstalls.length) return;
@ -505,6 +193,9 @@
updateNotesPanel(); updateNotesPanel();
$(items[currentIndex])[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); $(items[currentIndex])[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}); });
// Подключение к WebSocket
connectWebSocket();
}); });
function toggleTheme() { 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() { function loadFolders() {
$.getJSON(`${API_URL}/folders`, function (data) { $.getJSON(`${API_URL}/folders`, function (data) {
allFolders = [...data].sort((a, b) => a.name.localeCompare(b.name)); 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; let filtered = folderId ? data.filter(i => i.folder_id == folderId && i.folder_id !== null) : data;
displayInstalls(filtered); displayInstalls(filtered);
updateNotesPanel(); updateNotesPanel();
// Восстанавливаем выделение после перерендера
if (selectedInstallId) { if (selectedInstallId) {
const items = $('#installs-list .install-item');
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected'); $(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
} }
}).fail(function(jqxhr, textStatus, error) { }).fail(function(jqxhr, textStatus, error) {
@ -724,17 +431,21 @@
if (installs.length === 0) { if (installs.length === 0) {
$('#installs-list').html('<p>Нет результатов</p>'); $('#installs-list').html('<p>Нет результатов</p>');
} else { } else {
$('#installs-list').html(installs.map(item => ` $('#installs-list').html(installs.map(item => {
<div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true"> const lastSeen = new Date(item.last_seen || '1970-01-01');
<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> const isOnline = (new Date() - lastSeen) < 60000; // Онлайн, если менее 1 минуты
<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> const statusClass = isOnline ? 'status-online' : 'status-offline';
<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> return `
<div class="actions"><span class="action-buttons"><button onclick="deleteInstall(${item.id})">Удалить</button> <button class="note-button">Заметка</button></span></div> <div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true">
</div> <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>
`).join('')); <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(); updateSortArrows();
// Восстанавливаем выделение после перерендера
if (selectedInstallId) { if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected'); $(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
} }
@ -907,7 +618,7 @@
const install = allInstalls.find(i => i.id === installId); const install = allInstalls.find(i => i.id === installId);
if (install) { if (install) {
data['protocol'] = install.protocol; data['protocol'] = install.protocol;
data['note'] = install.note; // Сохраняем текущую заметку data['note'] = install.note;
} }
$.ajax({ $.ajax({
url: `${API_URL}/install/${installId}`, url: `${API_URL}/install/${installId}`,
@ -947,7 +658,6 @@
console.error('Install not found:', installId); console.error('Install not found:', installId);
return; return;
} }
// Передаем все текущие данные записи, чтобы ничего не очистилось
$.ajax({ $.ajax({
url: `${API_URL}/install/${installId}`, url: `${API_URL}/install/${installId}`,
type: 'PUT', type: 'PUT',
@ -958,7 +668,7 @@
install_time: install.install_time, install_time: install.install_time,
folder_id: folderId, folder_id: folderId,
protocol: install.protocol, protocol: install.protocol,
note: install.note // Явно передаем заметку note: install.note
}), }),
success: function () { success: function () {
console.log('Move successful'); console.log('Move successful');
@ -1008,7 +718,7 @@
function openNoteModal(installId, currentNote) { function openNoteModal(installId, currentNote) {
console.log('Opening note modal for installId:', installId, 'with note:', currentNote); console.log('Opening note modal for installId:', installId, 'with note:', currentNote);
selectedInstallId = installId; // Сохраняем выделение selectedInstallId = installId;
$('#note-textarea').val(currentNote || ''); $('#note-textarea').val(currentNote || '');
$('#note-modal').show(); $('#note-modal').show();
$('#modal-overlay').show(); $('#modal-overlay').show();
@ -1063,7 +773,7 @@
const note = $('#note-textarea').val() || ''; const note = $('#note-textarea').val() || '';
const install = allInstalls.find(i => i.id === selectedInstallId); const install = allInstalls.find(i => i.id === selectedInstallId);
if (install) { if (install) {
const currentSelectedId = selectedInstallId; // Сохраняем текущий selectedInstallId const currentSelectedId = selectedInstallId;
$.ajax({ $.ajax({
url: `${API_URL}/install/${selectedInstallId}`, url: `${API_URL}/install/${selectedInstallId}`,
type: 'PUT', type: 'PUT',
@ -1078,11 +788,9 @@
}), }),
success: function () { success: function () {
console.log('Note saved successfully'); console.log('Note saved successfully');
loadInstalls(selectedFolderId); // Перезагружаем список loadInstalls(selectedFolderId);
selectedInstallId = currentSelectedId; // Восстанавливаем selectedInstallId selectedInstallId = currentSelectedId;
closeNoteModal(); // Закрываем модальное окно closeNoteModal();
// Восстанавливаем выделение
const items = $('#installs-list .install-item');
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected'); $(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
updateNotesPanel(); updateNotesPanel();
}, },
@ -1099,19 +807,12 @@
$('#modal-overlay').hide(); $('#modal-overlay').hide();
$('#note-textarea').val(''); $('#note-textarea').val('');
document.getElementById('image-upload-input').value = ''; document.getElementById('image-upload-input').value = '';
// Фокус остаётся на текущей записи
if (selectedInstallId) { if (selectedInstallId) {
const items = $('#installs-list .install-item');
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected'); $(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
updateNotesPanel(); updateNotesPanel();
} }
} }
function closeAddModal() {
$('#add-modal').hide();
$('#modal-overlay').hide();
}
function updateNotesPanel() { function updateNotesPanel() {
if (selectedInstallId) { if (selectedInstallId) {
const install = allInstalls.find(i => i.id === selectedInstallId); const install = allInstalls.find(i => i.id === selectedInstallId);