main
Satur@it-depot.ru 2025-03-05 11:19:08 +03:00
parent 881210cd04
commit 22fa5e55af
2 changed files with 104 additions and 19 deletions

39
app.py
View File

@ -3,6 +3,7 @@ from pydantic import BaseModel
import sqlite3 import sqlite3
import datetime import datetime
import csv import csv
import markdown
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
import io import io
@ -19,8 +20,11 @@ columns = [row[1] for row in cursor.fetchall()]
if 'protocol' not in columns: if 'protocol' not in columns:
cursor.execute("ALTER TABLE installs ADD COLUMN protocol TEXT DEFAULT 'rustdesk'") cursor.execute("ALTER TABLE installs ADD COLUMN protocol TEXT DEFAULT 'rustdesk'")
conn.commit() conn.commit()
if 'note' not in columns:
cursor.execute("ALTER TABLE installs ADD COLUMN note TEXT DEFAULT ''")
conn.commit()
# Создаем таблицы (добавляем поле protocol, если таблицы нет) # Создаем таблицы (добавляем поле protocol и note, если таблицы нет)
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS folders ( CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -37,6 +41,7 @@ CREATE TABLE IF NOT EXISTS installs (
install_time TEXT, install_time TEXT,
folder_id INTEGER, folder_id INTEGER,
protocol TEXT DEFAULT 'rustdesk', protocol TEXT DEFAULT 'rustdesk',
note TEXT DEFAULT '',
FOREIGN KEY (folder_id) REFERENCES folders(id) FOREIGN KEY (folder_id) REFERENCES folders(id)
) )
""") """)
@ -66,6 +71,7 @@ class InstallData(BaseModel):
install_time: str | None = None # Принимаем строку в формате YYYY-MM-DD HH:MM:SS install_time: str | None = None # Принимаем строку в формате YYYY-MM-DD HH:MM:SS
folder_id: int | None = None folder_id: int | None = None
protocol: str | None = 'rustdesk' # По умолчанию RustDesk protocol: str | None = 'rustdesk' # По умолчанию RustDesk
note: str | None = '' # Новая заметка, по умолчанию пустая
# Монтируем папки templates и icons как статические файлы # Монтируем папки templates и icons как статические файлы
app.mount("/templates", StaticFiles(directory="templates"), name="templates") app.mount("/templates", StaticFiles(directory="templates"), name="templates")
@ -116,7 +122,7 @@ def delete_folder(folder_id: int):
@app.get("/api/installs") @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 SELECT i.id, i.rust_id, i.computer_name, i.install_time, i.folder_id, f.name as folder_name, i.protocol, i.note
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
""") """)
@ -139,7 +145,7 @@ def get_installs():
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]} "folder_id": row[4], "folder_name": row[5], "protocol": row[6], "note": row[7]}
for row in rows] for row in rows]
@app.post("/api/install") @app.post("/api/install")
@ -147,14 +153,15 @@ def add_install(data: InstallData):
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' # По умолчанию RustDesk для POST-запросов protocol = data.protocol or 'rustdesk' # По умолчанию RustDesk для POST-запросов
cursor.execute("INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol) VALUES (?, ?, ?, ?, ?)", note = data.note or '' # Новая заметка, по умолчанию пустая
(data.rust_id, data.computer_name, install_time, folder_id, protocol)) 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))
conn.commit() conn.commit()
return {"status": "success"} return {"status": "success"}
@app.put("/api/install/{install_id}") @app.put("/api/install/{install_id}")
def update_install(install_id: int, data: InstallData): def update_install(install_id: int, data: InstallData):
cursor.execute("SELECT rust_id, computer_name, install_time, folder_id, protocol FROM installs WHERE id = ?", (install_id,)) cursor.execute("SELECT rust_id, computer_name, install_time, folder_id, protocol, note FROM installs WHERE id = ?", (install_id,))
current = cursor.fetchone() current = cursor.fetchone()
if not current: if not current:
raise HTTPException(status_code=404, detail="Запись не найдена") raise HTTPException(status_code=404, detail="Запись не найдена")
@ -164,6 +171,7 @@ def update_install(install_id: int, data: InstallData):
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]
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]
# Форматируем время, если оно предоставлено в запросе # Форматируем время, если оно предоставлено в запросе
if new_install_time: if new_install_time:
@ -177,9 +185,9 @@ def update_install(install_id: int, data: InstallData):
cursor.execute(""" cursor.execute("""
UPDATE installs UPDATE installs
SET rust_id = ?, computer_name = ?, install_time = ?, folder_id = ?, protocol = ? SET rust_id = ?, computer_name = ?, install_time = ?, folder_id = ?, protocol = ?, note = ?
WHERE id = ? WHERE id = ?
""", (new_rust_id, new_computer_name, new_install_time, new_folder_id, new_protocol, install_id)) """, (new_rust_id, new_computer_name, new_install_time, new_folder_id, new_protocol, new_note, install_id))
conn.commit() conn.commit()
return {"status": "success"} return {"status": "success"}
@ -196,14 +204,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 SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note
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 SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note
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
""") """)
@ -211,11 +219,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]]) writer.writerow([row[0], row[1], install_time, row[3], row[4], row[5]])
headers = { headers = {
'Content-Disposition': 'attachment; filename="rustdesk_data.csv"', 'Content-Disposition': 'attachment; filename="rustdesk_data.csv"',
@ -253,6 +261,7 @@ async def import_csv(file: UploadFile = File(...), folder_id: int | None = Query
install_time = row['Время установки'] install_time = row['Время установки']
folder_name = row.get('Папка', None) folder_name = row.get('Папка', None)
protocol = row.get('Протокол', 'rustdesk') protocol = row.get('Протокол', 'rustdesk')
note = row.get('Заметка', '')
# Проверяем, существует ли запись с таким rust_id # Проверяем, существует ли запись с таким rust_id
cursor.execute("SELECT id FROM installs WHERE rust_id = ?", (rust_id,)) cursor.execute("SELECT id FROM installs WHERE rust_id = ?", (rust_id,))
@ -282,9 +291,9 @@ async def import_csv(file: UploadFile = File(...), folder_id: int | None = Query
folder_id = target_folder_id folder_id = target_folder_id
cursor.execute(""" cursor.execute("""
INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol) INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol, note)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", (rust_id, computer_name, install_time, folder_id, protocol)) """, (rust_id, computer_name, install_time, folder_id, protocol, note))
conn.commit() conn.commit()
return {"status": "success", "message": "Данные успешно импортированы"} return {"status": "success", "message": "Данные успешно импортированы"}

View File

@ -5,6 +5,7 @@
<title>Органайзер RustDesk</title> <title>Органайзер RustDesk</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/jstree.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/jstree.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <!-- Библиотека для Markdown -->
<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; font-family: Arial, sans-serif; } body { display: flex; font-family: Arial, sans-serif; }
@ -29,6 +30,12 @@
#import-label:hover { background: #0056b3; } #import-label:hover { background: #0056b3; }
.folder-select { margin: 5px 0; } .folder-select { margin: 5px 0; }
.time-hint { font-size: 12px; color: #666; margin-top: 5px; } .time-hint { font-size: 12px; color: #666; margin-top: 5px; }
#notes-panel { width: 100%; padding: 10px; border-left: 1px solid #ccc; background: #f9f9f9; min-height: 200px; }
#notes-content { margin-top: 10px; }
#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; }
#note-modal textarea { width: 100%; height: 200px; margin-bottom: 10px; }
#note-modal button { 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; }
</style> </style>
</head> </head>
<body> <body>
@ -71,6 +78,7 @@
<option value="vnc">VNC</option> <option value="vnc">VNC</option>
<option value="rdp">RDP</option> <option value="rdp">RDP</option>
</select> </select>
<textarea id="note" placeholder="Заметка (Markdown поддерживается)"></textarea>
<button onclick="addInstall()">Добавить</button> <button onclick="addInstall()">Добавить</button>
<div class="time-hint">Пример: 2025-03-05 14:30:00</div> <div class="time-hint">Пример: 2025-03-05 14:30:00</div>
</div> </div>
@ -82,11 +90,25 @@
</div> </div>
<div id="installs-list"></div> <div id="installs-list"></div>
</div> </div>
<div id="notes-panel">
<h2>Заметка</h2>
<div id="notes-content"></div>
</div>
<!-- Модальное окно для редактирования заметок -->
<div id="modal-overlay"></div>
<div id="note-modal">
<h3>Редактировать заметку</h3>
<textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
<button onclick="saveNote()">Сохранить</button>
<button onclick="closeNoteModal()">Закрыть</button>
</div>
<script> <script>
const API_URL = "/api"; const API_URL = "/api";
let selectedFolderId = null; let selectedFolderId = null;
let allInstalls = []; let allInstalls = [];
let selectedInstallId = null;
let sortField = null; let sortField = null;
let sortDirection = 'asc'; let sortDirection = 'asc';
@ -181,6 +203,7 @@
allInstalls = data; allInstalls = data;
let filtered = folderId ? data.filter(i => i.folder_id == folderId) : data.filter(i => i.folder_id === null); let filtered = folderId ? data.filter(i => i.folder_id == folderId) : data.filter(i => i.folder_id === null);
displayInstalls(filtered); displayInstalls(filtered);
updateNotesPanel(); // Обновляем заметки после загрузки
}); });
selectedFolderId = folderId; selectedFolderId = folderId;
} }
@ -192,8 +215,9 @@
<div style="flex: 1; text-align: center;">${item.computer_name}</div> <div style="flex: 1; text-align: center;">${item.computer_name}</div>
<div style="flex: 1; text-align: center;">${item.install_time}</div> <div style="flex: 1; text-align: center;">${item.install_time}</div>
<div style="flex: 1; text-align: center;"> <div style="flex: 1; text-align: center;">
<button onclick="editInstall(${item.id}, '${item.rust_id}', '${item.computer_name}', '${item.install_time}', '${item.protocol}')">Редактировать</button> <button onclick="editInstall(${item.id}, '${item.rust_id}', '${item.computer_name}', '${item.install_time}', '${item.protocol}', '${item.note}')">Редактировать</button>
<button onclick="deleteInstall(${item.id})">Удалить</button> <button onclick="deleteInstall(${item.id})">Удалить</button>
<button onclick="openNoteModal(${item.id}, '${item.note}')">Заметка</button>
</div> </div>
</div> </div>
`).join('')); `).join(''));
@ -306,6 +330,7 @@
const computerName = $('#computerName').val(); const computerName = $('#computerName').val();
const installTime = $('#installTime').val() || ''; // Пустая строка, если не указано const installTime = $('#installTime').val() || ''; // Пустая строка, если не указано
const protocol = $('#protocol').val(); const protocol = $('#protocol').val();
const note = $('#note').val() || ''; // Новая заметка из формы
if (installTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(installTime)) { if (installTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(installTime)) {
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)"); alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
@ -316,7 +341,7 @@
url: `${API_URL}/install`, url: `${API_URL}/install`,
type: 'POST', type: 'POST',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ rust_id: rustId, computer_name: computerName, install_time: installTime, folder_id: selectedFolderId, protocol: protocol }), data: JSON.stringify({ rust_id: rustId, computer_name: computerName, install_time: installTime, folder_id: selectedFolderId, protocol: protocol, note: note }),
success: function () { success: function () {
loadInstalls(selectedFolderId); loadInstalls(selectedFolderId);
}, },
@ -330,11 +355,12 @@
}); });
} }
function editInstall(id, rustId, computerName, installTime, protocol) { function editInstall(id, rustId, computerName, installTime, protocol, note) {
const newRustId = prompt("ID подключения:", rustId); const newRustId = prompt("ID подключения:", rustId);
const newComputerName = prompt("Имя компьютера:", computerName); const newComputerName = prompt("Имя компьютера:", computerName);
let newInstallTime = prompt("Время установки (опционально, формат: ГГГГ-ММ-ДД ЧЧ:ММ:СС):", installTime || ''); let newInstallTime = prompt("Время установки (опционально, формат: ГГГГ-ММ-ДД ЧЧ:ММ:СС):", installTime || '');
const newProtocol = prompt("Протокол (rustdesk, anydesk, ammyy, teamviewer, vnc, rdp):", protocol); const newProtocol = prompt("Протокол (rustdesk, anydesk, ammyy, teamviewer, vnc, rdp):", protocol);
const newNote = prompt("Заметка (Markdown поддерживается):", note || '');
if (newInstallTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(newInstallTime)) { if (newInstallTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(newInstallTime)) {
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)"); alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
@ -346,7 +372,7 @@
url: `${API_URL}/install/${id}`, url: `${API_URL}/install/${id}`,
type: 'PUT', type: 'PUT',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ rust_id: newRustId, computer_name: newComputerName, install_time: newInstallTime, folder_id: selectedFolderId, protocol: newProtocol }), data: JSON.stringify({ rust_id: newRustId, computer_name: newComputerName, install_time: newInstallTime, folder_id: selectedFolderId, protocol: newProtocol, note: newNote }),
success: function () { success: function () {
loadInstalls(selectedFolderId); loadInstalls(selectedFolderId);
}, },
@ -425,6 +451,56 @@
} }
}); });
} }
function openNoteModal(installId, currentNote) {
selectedInstallId = installId;
$('#note-textarea').val(currentNote);
$('#note-modal').show();
$('#modal-overlay').show();
}
function saveNote() {
const note = $('#note-textarea').val() || '';
$.ajax({
url: `${API_URL}/install/${selectedInstallId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ note: note }),
success: function () {
loadInstalls(selectedFolderId);
closeNoteModal();
},
error: function (xhr, status, error) {
console.error("Ошибка сохранения заметки:", status, error);
alert("Не удалось сохранить заметку");
}
});
}
function closeNoteModal() {
$('#note-modal').hide();
$('#modal-overlay').hide();
$('#note-textarea').val('');
}
function updateNotesPanel() {
if (selectedInstallId) {
const install = allInstalls.find(i => i.id === selectedInstallId);
if (install && install.note) {
$('#notes-content').html(marked.parse(install.note)); // Рендеринг Markdown
} else {
$('#notes-content').html('<p>Нет заметок</p>');
}
} else {
$('#notes-content').html('<p>Выберите подключение для просмотра заметки</p>');
}
}
// Обновляем заметки при клике на подключение
$('#installs-list').on('click', '.install-item', function () {
selectedInstallId = $(this).data('id');
updateNotesPanel();
});
</script> </script>
</body> </body>
</html> </html>