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
|
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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,5 @@ fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
python-multipart
|
python-multipart
|
||||||
markdown
|
markdown
|
||||||
werkzeug
|
werkzeug
|
||||||
|
websockets
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue