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
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}

View File

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

View File

@ -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);