mirror of
https://github.com/g0ldyy/comet.git
synced 2026-01-12 01:16:12 +01:00
feat: implement new admin dashboard
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ USE_GUNICORN=True # Will use uvicorn if False or if on Windows
|
||||
# ============================== #
|
||||
# Dashboard Settings #
|
||||
# ============================== #
|
||||
DASHBOARD_ADMIN_PASSWORD=CHANGE_ME # The password to access the dashboard
|
||||
ADMIN_DASHBOARD_PASSWORD=CHANGE_ME # The password to access the dashboard
|
||||
|
||||
# ============================== #
|
||||
# Database Configuration #
|
||||
|
||||
+203
-21
@@ -2,20 +2,144 @@ import random
|
||||
import string
|
||||
import secrets
|
||||
import orjson
|
||||
import uuid
|
||||
import time
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
from loguru import logger as loguru_logger
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Form, Cookie
|
||||
from fastapi.responses import RedirectResponse, Response, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.security import HTTPBasic
|
||||
|
||||
from comet.utils.models import settings, web_config, database
|
||||
from comet.utils.general import config_check
|
||||
from comet.utils.log_levels import get_level_info
|
||||
from comet.debrid.manager import get_debrid_extension
|
||||
|
||||
templates = Jinja2Templates("comet/templates")
|
||||
main = APIRouter()
|
||||
security = HTTPBasic()
|
||||
|
||||
admin_sessions = {}
|
||||
|
||||
|
||||
class LogCapture:
|
||||
def __init__(self):
|
||||
self.logs = []
|
||||
self.max_logs = 1000
|
||||
|
||||
def add_log(self, record):
|
||||
# Format the log record similar to loguru format
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
|
||||
level_name = record.levelname
|
||||
|
||||
# Handle special loguru levels
|
||||
if hasattr(record, "extra") and "level_name" in record.extra:
|
||||
level_name = record.extra["level_name"]
|
||||
|
||||
level_info = get_level_info(level_name)
|
||||
|
||||
log_entry = {
|
||||
"timestamp": timestamp,
|
||||
"level": level_name,
|
||||
"icon": level_info["icon"],
|
||||
"color": level_info["color"],
|
||||
"module": getattr(record, "module", "unknown"),
|
||||
"function": getattr(record, "funcName", "unknown"),
|
||||
"message": record.getMessage(),
|
||||
"created": record.created,
|
||||
}
|
||||
|
||||
self.logs.append(log_entry)
|
||||
if len(self.logs) > self.max_logs:
|
||||
self.logs.pop(0)
|
||||
|
||||
def get_logs(self):
|
||||
return self.logs
|
||||
|
||||
|
||||
# Global log capture instance
|
||||
log_capture = LogCapture()
|
||||
|
||||
|
||||
# Loguru handler to capture logs
|
||||
class LoguruHandler:
|
||||
def __init__(self, log_capture):
|
||||
self.log_capture = log_capture
|
||||
|
||||
def write(self, message):
|
||||
if message.strip():
|
||||
# Try to extract timestamp, level, module, function, and message
|
||||
# This is a simplified parser for loguru format
|
||||
pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \| ([🌠👾👻🎬🔒📰🕸️⚠️❌💀]?) ?(\w+) \| (\w+)\.(\w+) - (.+)"
|
||||
match = re.match(pattern, message.strip())
|
||||
|
||||
if match:
|
||||
timestamp_str, icon, level, module, function, msg = match.groups()
|
||||
|
||||
level_info = get_level_info(level)
|
||||
|
||||
log_entry = {
|
||||
"timestamp": timestamp_str,
|
||||
"level": level,
|
||||
"icon": icon or level_info["icon"],
|
||||
"color": level_info["color"],
|
||||
"module": module,
|
||||
"function": function,
|
||||
"message": msg,
|
||||
"created": time.time(),
|
||||
}
|
||||
|
||||
self.log_capture.add_log_entry(log_entry)
|
||||
|
||||
|
||||
# Add method to log capture to handle parsed entries
|
||||
def add_log_entry_to_capture(self, log_entry):
|
||||
self.logs.append(log_entry)
|
||||
if len(self.logs) > self.max_logs:
|
||||
self.logs.pop(0)
|
||||
|
||||
|
||||
# Monkey patch the method
|
||||
log_capture.add_log_entry = add_log_entry_to_capture.__get__(log_capture, LogCapture)
|
||||
|
||||
# Set up loguru handler
|
||||
loguru_handler = LoguruHandler(log_capture)
|
||||
|
||||
# Add our handler to loguru
|
||||
loguru_logger.add(
|
||||
loguru_handler.write,
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level.icon} {level} | {module}.{function} - {message}",
|
||||
)
|
||||
|
||||
|
||||
def create_admin_session() -> str:
|
||||
session_id = str(uuid.uuid4())
|
||||
admin_sessions[session_id] = {"created_at": time.time()}
|
||||
return session_id
|
||||
|
||||
|
||||
def verify_admin_session(admin_session: str = Cookie(None)):
|
||||
if not admin_session or admin_session not in admin_sessions:
|
||||
return False
|
||||
|
||||
session_data = admin_sessions[admin_session]
|
||||
|
||||
# Check if session is older than 24 hours
|
||||
if time.time() - session_data["created_at"] > 86400:
|
||||
del admin_sessions[admin_session]
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def require_admin_auth(admin_session: str = Cookie(None)):
|
||||
if not verify_admin_session(admin_session):
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
|
||||
@main.get("/")
|
||||
async def root():
|
||||
@@ -80,29 +204,87 @@ async def manifest(request: Request, b64config: str = None):
|
||||
return base_manifest
|
||||
|
||||
|
||||
class CustomORJSONResponse(Response):
|
||||
media_type = "application/json"
|
||||
|
||||
def render(self, content):
|
||||
return orjson.dumps(content, option=orjson.OPT_INDENT_2)
|
||||
@main.get("/admin")
|
||||
async def admin_root(request: Request, admin_session: str = Cookie(None)):
|
||||
if verify_admin_session(admin_session):
|
||||
return RedirectResponse("/admin/dashboard")
|
||||
return templates.TemplateResponse("admin_login.html", {"request": request})
|
||||
|
||||
|
||||
def verify_dashboard_auth(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
is_correct = secrets.compare_digest(
|
||||
credentials.password, settings.DASHBOARD_ADMIN_PASSWORD
|
||||
)
|
||||
@main.post("/admin/login")
|
||||
async def admin_login(request: Request, password: str = Form(...)):
|
||||
is_correct = secrets.compare_digest(password, settings.ADMIN_DASHBOARD_PASSWORD)
|
||||
|
||||
if not is_correct:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Incorrect password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
return templates.TemplateResponse(
|
||||
"admin_login.html", {"request": request, "error": "Invalid password"}
|
||||
)
|
||||
|
||||
return True
|
||||
session_id = create_admin_session()
|
||||
response = RedirectResponse("/admin/dashboard", status_code=303)
|
||||
response.set_cookie(
|
||||
key="admin_session",
|
||||
value=session_id,
|
||||
httponly=True,
|
||||
secure=False,
|
||||
samesite="lax",
|
||||
max_age=86400,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@main.get("/dashboard", response_class=CustomORJSONResponse)
|
||||
async def dashboard(authenticated: bool = Depends(verify_dashboard_auth)):
|
||||
rows = await database.fetch_all("SELECT * FROM active_connections")
|
||||
return rows
|
||||
@main.get("/admin/dashboard")
|
||||
async def admin_dashboard(request: Request, admin_session: str = Cookie(None)):
|
||||
try:
|
||||
require_admin_auth(admin_session)
|
||||
return templates.TemplateResponse("admin_dashboard.html", {"request": request})
|
||||
except HTTPException:
|
||||
return RedirectResponse("/admin", status_code=303)
|
||||
|
||||
|
||||
@main.post("/admin/logout")
|
||||
async def admin_logout(admin_session: str = Cookie(None)):
|
||||
if admin_session and admin_session in admin_sessions:
|
||||
del admin_sessions[admin_session]
|
||||
|
||||
response = RedirectResponse("/admin", status_code=303)
|
||||
response.delete_cookie("admin_session")
|
||||
return response
|
||||
|
||||
|
||||
@main.get("/admin/api/connections")
|
||||
async def admin_api_connections(admin_session: str = Cookie(None)):
|
||||
require_admin_auth(admin_session)
|
||||
rows = await database.fetch_all(
|
||||
"SELECT id, ip, content, timestamp FROM active_connections ORDER BY timestamp DESC"
|
||||
)
|
||||
|
||||
connections = []
|
||||
for row in rows:
|
||||
connections.append(
|
||||
{
|
||||
"id": row["id"],
|
||||
"ip": row["ip"],
|
||||
"content": row["content"],
|
||||
"timestamp": row["timestamp"],
|
||||
"duration": time.time() - row["timestamp"],
|
||||
"formatted_time": time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S", time.localtime(row["timestamp"])
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return JSONResponse({"connections": connections})
|
||||
|
||||
|
||||
@main.get("/admin/api/logs")
|
||||
async def admin_api_logs(admin_session: str = Cookie(None), since: float = 0):
|
||||
require_admin_auth(admin_session)
|
||||
|
||||
# Get logs since the specified timestamp
|
||||
all_logs = log_capture.get_logs()
|
||||
new_logs = [log for log in all_logs if log["created"] > since]
|
||||
|
||||
return JSONResponse(
|
||||
{"logs": new_logs, "total_logs": len(all_logs), "new_logs": len(new_logs)}
|
||||
)
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ def start_log():
|
||||
)
|
||||
logger.log(
|
||||
"COMET",
|
||||
f"Dashboard Admin Password: {settings.DASHBOARD_ADMIN_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/dashboard",
|
||||
f"Admin Dashboard Password: {settings.ADMIN_DASHBOARD_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/admin",
|
||||
)
|
||||
logger.log(
|
||||
"COMET",
|
||||
|
||||
@@ -0,0 +1,725 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="sl-theme-dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta content="Comet" property="og:title" />
|
||||
<meta content="Comet Dashboard" property="og:description" />
|
||||
<meta content="#6b6ef8" data-react-helmet="true" name="theme-color" />
|
||||
|
||||
<title>Comet - Dashboard</title>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://i.imgur.com/jmVoVMu.jpeg"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/themes/dark.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/shoelace-autoloader.js"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
:not(:defined) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: radial-gradient(
|
||||
ellipse at bottom,
|
||||
#25292c 0%,
|
||||
#0c0d13 100%
|
||||
);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: #1a1d20;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.comet-text {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
background-color: #1a1d20;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 30px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.connections-table {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
border-collapse: collapse;
|
||||
background-color: #374151;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.connections-table th,
|
||||
.connections-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.connections-table th {
|
||||
background-color: #4b5563;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #f3f4f6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.connections-table td {
|
||||
color: #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.connections-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.connections-table tr:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.connection-id {
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: 0.75rem;
|
||||
background-color: #1f2937;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.ip-tag {
|
||||
background-color: #065f46;
|
||||
color: #d1fae5;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.duration-tag {
|
||||
background-color: #1e40af;
|
||||
color: #dbeafe;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background-color: #0f1419;
|
||||
border-radius: 0.375rem;
|
||||
padding: 20px;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: #9ca3af;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
font-weight: 600;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.log-module {
|
||||
color: #60a5fa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #374151;
|
||||
padding: 20px;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #9ca3af;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.auto-refresh-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state sl-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="comet-text">
|
||||
<img
|
||||
class="emoji"
|
||||
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f4ab/512.gif"
|
||||
/>
|
||||
Comet
|
||||
</h1>
|
||||
<div class="admin-badge">Dashboard</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="nav-links">
|
||||
<sl-button
|
||||
variant="neutral"
|
||||
size="small"
|
||||
onclick="window.open('/configure', '_blank')"
|
||||
>
|
||||
<sl-icon slot="prefix" name="gear"></sl-icon>
|
||||
Configuration
|
||||
</sl-button>
|
||||
</div>
|
||||
<form method="post" action="/admin/logout" style="margin: 0">
|
||||
<sl-button type="submit" variant="danger" size="small">
|
||||
<sl-icon slot="prefix" name="box-arrow-right"></sl-icon>
|
||||
Logout
|
||||
</sl-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="connections">
|
||||
<sl-icon name="wifi"></sl-icon>
|
||||
Active Connections
|
||||
</sl-tab>
|
||||
<sl-tab slot="nav" panel="logs">
|
||||
<sl-icon name="list-ul"></sl-icon>
|
||||
System Logs
|
||||
</sl-tab>
|
||||
|
||||
<sl-tab-panel name="connections">
|
||||
<div class="tab-content">
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="total-connections">0</div>
|
||||
<div class="stat-label">Active Connections</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="unique-ips">0</div>
|
||||
<div class="stat-label">Unique IPs</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="avg-duration">0s</div>
|
||||
<div class="stat-label">Avg Duration</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connections-content">
|
||||
<div class="empty-state">
|
||||
<sl-icon name="wifi-off"></sl-icon>
|
||||
<p>No active connections</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
|
||||
<sl-tab-panel name="logs">
|
||||
<div class="tab-content">
|
||||
<div class="logs-controls">
|
||||
<div>
|
||||
<sl-button id="clear-logs" variant="neutral" size="small">
|
||||
<sl-icon slot="prefix" name="trash"></sl-icon>
|
||||
Clear Display
|
||||
</sl-button>
|
||||
<sl-button id="download-logs" variant="neutral" size="small">
|
||||
<sl-icon slot="prefix" name="download"></sl-icon>
|
||||
Download Logs
|
||||
</sl-button>
|
||||
</div>
|
||||
<div class="auto-refresh-control">
|
||||
<sl-switch id="hide-api-logs" checked
|
||||
>Hide API Logs</sl-switch
|
||||
>
|
||||
<sl-switch id="auto-refresh" checked>Auto Refresh</sl-switch>
|
||||
<sl-select
|
||||
id="refresh-interval"
|
||||
value="2000"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
>
|
||||
<sl-option value="1000">1s</sl-option>
|
||||
<sl-option value="2000">2s</sl-option>
|
||||
<sl-option value="5000">5s</sl-option>
|
||||
<sl-option value="10000">10s</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-container" id="logs-container">
|
||||
<div class="empty-state">
|
||||
<sl-icon name="journal-text"></sl-icon>
|
||||
<p>Loading system logs...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await Promise.allSettled([
|
||||
customElements.whenDefined("sl-tab-group"),
|
||||
customElements.whenDefined("sl-tab"),
|
||||
customElements.whenDefined("sl-tab-panel"),
|
||||
customElements.whenDefined("sl-button"),
|
||||
customElements.whenDefined("sl-icon"),
|
||||
customElements.whenDefined("sl-spinner"),
|
||||
customElements.whenDefined("sl-switch"),
|
||||
customElements.whenDefined("sl-select"),
|
||||
customElements.whenDefined("sl-option"),
|
||||
]);
|
||||
|
||||
document.body.classList.add("ready");
|
||||
|
||||
// Initialize dashboard
|
||||
initializeDashboard();
|
||||
});
|
||||
|
||||
let connectionsInterval;
|
||||
let logsInterval;
|
||||
let lastLogTimestamp = 0;
|
||||
|
||||
function initializeDashboard() {
|
||||
// Load initial data
|
||||
loadConnections();
|
||||
loadLogs();
|
||||
|
||||
// Set up auto-refresh
|
||||
setupAutoRefresh();
|
||||
|
||||
// Event listeners
|
||||
document
|
||||
.getElementById("clear-logs")
|
||||
.addEventListener("click", clearLogsDisplay);
|
||||
document
|
||||
.getElementById("download-logs")
|
||||
.addEventListener("click", downloadLogs);
|
||||
document
|
||||
.getElementById("auto-refresh")
|
||||
.addEventListener("sl-change", toggleAutoRefresh);
|
||||
document
|
||||
.getElementById("refresh-interval")
|
||||
.addEventListener("sl-change", updateRefreshInterval);
|
||||
document
|
||||
.getElementById("hide-api-logs")
|
||||
.addEventListener("sl-change", toggleApiLogsVisibility);
|
||||
|
||||
// Tab change handler
|
||||
document
|
||||
.querySelector("sl-tab-group")
|
||||
.addEventListener("sl-tab-show", handleTabChange);
|
||||
}
|
||||
|
||||
function setupAutoRefresh() {
|
||||
// Connections refresh every 5 seconds
|
||||
connectionsInterval = setInterval(loadConnections, 5000);
|
||||
|
||||
// Logs refresh based on selected interval
|
||||
const interval = parseInt(
|
||||
document.getElementById("refresh-interval").value
|
||||
);
|
||||
logsInterval = setInterval(loadLogs, interval);
|
||||
}
|
||||
|
||||
function toggleAutoRefresh(e) {
|
||||
if (e.target.checked) {
|
||||
setupAutoRefresh();
|
||||
} else {
|
||||
clearInterval(connectionsInterval);
|
||||
clearInterval(logsInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRefreshInterval() {
|
||||
if (document.getElementById("auto-refresh").checked) {
|
||||
clearInterval(logsInterval);
|
||||
const interval = parseInt(
|
||||
document.getElementById("refresh-interval").value
|
||||
);
|
||||
logsInterval = setInterval(loadLogs, interval);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabChange(e) {
|
||||
const activePanel = e.detail.name;
|
||||
if (activePanel === "connections") {
|
||||
loadConnections();
|
||||
} else if (activePanel === "logs") {
|
||||
loadLogs();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConnections() {
|
||||
try {
|
||||
const response = await fetch("/admin/api/connections");
|
||||
const data = await response.json();
|
||||
|
||||
updateConnectionsStats(data.connections);
|
||||
renderConnectionsTable(data.connections);
|
||||
} catch (error) {
|
||||
console.error("Failed to load connections:", error);
|
||||
document.getElementById("connections-content").innerHTML =
|
||||
'<div class="empty-state"><sl-icon name="exclamation-triangle"></sl-icon><p>Failed to load connections</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnectionsStats(connections) {
|
||||
const totalConnections = connections.length;
|
||||
const uniqueIPs = [...new Set(connections.map((c) => c.ip))].length;
|
||||
const avgDuration =
|
||||
connections.length > 0
|
||||
? Math.round(
|
||||
connections.reduce((sum, c) => sum + c.duration, 0) /
|
||||
connections.length
|
||||
)
|
||||
: 0;
|
||||
|
||||
document.getElementById("total-connections").textContent =
|
||||
totalConnections;
|
||||
document.getElementById("unique-ips").textContent = uniqueIPs;
|
||||
document.getElementById("avg-duration").textContent = `${avgDuration}s`;
|
||||
}
|
||||
|
||||
function renderConnectionsTable(connections) {
|
||||
const container = document.getElementById("connections-content");
|
||||
|
||||
if (connections.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="empty-state"><sl-icon name="wifi-off"></sl-icon><p>No active connections</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tableHTML = `
|
||||
<table class="connections-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Connection ID</th>
|
||||
<th>IP Address</th>
|
||||
<th>Content</th>
|
||||
<th>Started</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${connections
|
||||
.map(
|
||||
(conn) => `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="connection-id">${conn.id.substring(
|
||||
0,
|
||||
8
|
||||
)}...</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="ip-tag">${conn.ip}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="${escapeHtml(
|
||||
conn.content
|
||||
)}">${
|
||||
conn.content.length > 40
|
||||
? escapeHtml(
|
||||
conn.content.substring(0, 40)
|
||||
) + "..."
|
||||
: escapeHtml(conn.content)
|
||||
}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style="font-size: 0.875rem; color: #9ca3af;">${
|
||||
conn.formatted_time
|
||||
}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="duration-tag">${Math.round(
|
||||
conn.duration
|
||||
)}s</span>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.innerHTML = tableHTML;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/admin/api/logs?since=${lastLogTimestamp}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
appendLogs(data.logs);
|
||||
|
||||
if (data.logs.length > 0) {
|
||||
lastLogTimestamp = Math.max(...data.logs.map((log) => log.created));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load logs:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function appendLogs(logs) {
|
||||
const container = document.getElementById("logs-container");
|
||||
const hideApiLogs = document.getElementById("hide-api-logs").checked;
|
||||
|
||||
// Remove empty state if present
|
||||
if (container.querySelector(".empty-state")) {
|
||||
container.innerHTML = "";
|
||||
}
|
||||
|
||||
logs.forEach((log) => {
|
||||
// Filter out API logs if hideApiLogs is enabled
|
||||
if (hideApiLogs && isApiLog(log)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logEntry = document.createElement("div");
|
||||
logEntry.className = "log-entry";
|
||||
|
||||
// Add data attribute to identify API logs for toggling
|
||||
if (isApiLog(log)) {
|
||||
logEntry.setAttribute("data-api-log", "true");
|
||||
}
|
||||
|
||||
logEntry.innerHTML = `<span class="log-timestamp">${
|
||||
log.timestamp
|
||||
}</span> <span class="log-level" style="color: ${log.color};">${
|
||||
log.icon
|
||||
} ${log.level}</span> <span class="log-module">${log.module}.${
|
||||
log.function
|
||||
}</span> - <span class="log-message">${escapeHtml(
|
||||
log.message
|
||||
)}</span>`;
|
||||
|
||||
container.appendChild(logEntry);
|
||||
});
|
||||
|
||||
// Auto-scroll to bottom
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
// Limit number of log entries to prevent memory issues
|
||||
const maxEntries = 500;
|
||||
while (container.children.length > maxEntries) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogsDisplay() {
|
||||
document.getElementById("logs-container").innerHTML = "";
|
||||
lastLogTimestamp = Date.now() / 1000;
|
||||
}
|
||||
|
||||
function downloadLogs() {
|
||||
const logEntries = Array.from(document.querySelectorAll(".log-entry"));
|
||||
const logText = logEntries.map((entry) => entry.textContent).join("\n");
|
||||
|
||||
const blob = new Blob([logText], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `comet-logs-${new Date()
|
||||
.toISOString()
|
||||
.slice(0, 19)
|
||||
.replace(/:/g, "-")}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function isApiLog(log) {
|
||||
return log.message.includes("/admin/api/");
|
||||
}
|
||||
|
||||
function toggleApiLogsVisibility() {
|
||||
const hideApiLogs = document.getElementById("hide-api-logs").checked;
|
||||
const container = document.getElementById("logs-container");
|
||||
const apiLogEntries = container.querySelectorAll(
|
||||
'[data-api-log="true"]'
|
||||
);
|
||||
|
||||
apiLogEntries.forEach((entry) => {
|
||||
entry.style.display = hideApiLogs ? "none" : "block";
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,333 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="sl-theme-dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta content="Comet" property="og:title" />
|
||||
<meta content="Comet Dashboard Login" property="og:description" />
|
||||
<meta content="#6b6ef8" data-react-helmet="true" name="theme-color" />
|
||||
|
||||
<title>Comet - Login</title>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://i.imgur.com/jmVoVMu.jpeg"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/themes/dark.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/shoelace-autoloader.js"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
:not(:defined) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at bottom,
|
||||
#25292c 0%,
|
||||
#0c0d13 100%
|
||||
);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.comet-text {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.admin-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-color: #1a1d20;
|
||||
padding: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.25);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stars {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 120%;
|
||||
transform: rotate(-45deg);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.star {
|
||||
--star-color: var(--primary-color);
|
||||
--star-tail-length: 6em;
|
||||
--star-tail-height: 2px;
|
||||
--star-width: calc(var(--star-tail-length) / 6);
|
||||
--fall-duration: 9s;
|
||||
--tail-fade-duration: var(--fall-duration);
|
||||
position: absolute;
|
||||
top: var(--top-offset);
|
||||
left: 0;
|
||||
width: var(--star-tail-length);
|
||||
height: var(--star-tail-height);
|
||||
color: var(--star-color);
|
||||
background: linear-gradient(45deg, currentColor, transparent);
|
||||
border-radius: 50%;
|
||||
filter: drop-shadow(0 0 6px currentColor);
|
||||
transform: translate3d(104em, 0, 0);
|
||||
animation: fall var(--fall-duration) var(--fall-delay) linear infinite,
|
||||
tail-fade var(--tail-fade-duration) var(--fall-delay) ease-out
|
||||
infinite;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
.star {
|
||||
animation: fall var(--fall-duration) var(--fall-delay) linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.star:nth-child(1) {
|
||||
--star-tail-length: 6.14em;
|
||||
--top-offset: 3.64vh;
|
||||
--fall-duration: 10.878s;
|
||||
--fall-delay: 0.034s;
|
||||
}
|
||||
|
||||
.star:nth-child(2) {
|
||||
--star-tail-length: 5.08em;
|
||||
--top-offset: 69.69vh;
|
||||
--fall-duration: 11.372s;
|
||||
--fall-delay: 1.679s;
|
||||
}
|
||||
|
||||
.star:nth-child(3) {
|
||||
--star-tail-length: 6.1em;
|
||||
--top-offset: 64.3vh;
|
||||
--fall-duration: 7.088s;
|
||||
--fall-delay: 3.382s;
|
||||
}
|
||||
|
||||
.star:nth-child(4) {
|
||||
--star-tail-length: 6.66em;
|
||||
--top-offset: 34.91vh;
|
||||
--fall-duration: 6.184s;
|
||||
--fall-delay: 9.61s;
|
||||
}
|
||||
|
||||
.star:nth-child(5) {
|
||||
--star-tail-length: 6.89em;
|
||||
--top-offset: 24.92vh;
|
||||
--fall-duration: 9.465s;
|
||||
--fall-delay: 9.12s;
|
||||
}
|
||||
|
||||
.star::before,
|
||||
.star::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: calc(var(--star-width) / -2);
|
||||
width: var(--star-width);
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
currentColor,
|
||||
transparent
|
||||
);
|
||||
border-radius: inherit;
|
||||
animation: blink 2s linear infinite;
|
||||
}
|
||||
|
||||
.star::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.star::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
to {
|
||||
transform: translate3d(-30em, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tail-fade {
|
||||
0%,
|
||||
50% {
|
||||
width: var(--star-tail-length);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
70%,
|
||||
80% {
|
||||
width: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="stars"></div>
|
||||
|
||||
<script>
|
||||
let hasTouchScreen = false;
|
||||
if ("maxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
} else if ("msMaxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
const mQ = matchMedia?.("(pointer:coarse)");
|
||||
if (mQ?.media === "(pointer:coarse)") {
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
} else if ("orientation" in window) {
|
||||
hasTouchScreen = true;
|
||||
} else {
|
||||
const UA = navigator.userAgent;
|
||||
hasTouchScreen =
|
||||
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTouchScreen) {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const starsContainer = document.querySelector(".stars");
|
||||
const starCount = 15;
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const star = document.createElement("div");
|
||||
star.classList.add("star");
|
||||
starsContainer.appendChild(star);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<p class="comet-text">
|
||||
<img
|
||||
class="emoji"
|
||||
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f4ab/512.gif"
|
||||
/>
|
||||
Comet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="login-container">
|
||||
{% if error %}
|
||||
<div class="form-item error-message">
|
||||
<sl-alert variant="danger" open>
|
||||
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
|
||||
<strong>{{ error }}</strong>
|
||||
</sl-alert>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/login">
|
||||
<div class="form-item">
|
||||
<sl-input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
label="Admin Password"
|
||||
placeholder="Enter admin password"
|
||||
required
|
||||
autofocus
|
||||
></sl-input>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<sl-button type="submit" variant="primary" class="login-button">
|
||||
Sign In
|
||||
</sl-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await Promise.allSettled([
|
||||
customElements.whenDefined("sl-button"),
|
||||
customElements.whenDefined("sl-alert"),
|
||||
customElements.whenDefined("sl-input"),
|
||||
customElements.whenDefined("sl-icon"),
|
||||
]);
|
||||
|
||||
document.body.classList.add("ready");
|
||||
|
||||
// Handle form submission with loading state
|
||||
const form = document.querySelector("form");
|
||||
const submitButton = document.querySelector('sl-button[type="submit"]');
|
||||
|
||||
form.addEventListener("submit", function () {
|
||||
submitButton.loading = true;
|
||||
submitButton.textContent = "Signing In...";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1122
-934
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
STANDARD_LOG_LEVELS = {
|
||||
"DEBUG": {"color": "#DC5F00", "icon": "🕸️", "loguru_color": "<fg #DC5F00>"},
|
||||
"INFO": {"color": "#FC5F39", "icon": "📰", "loguru_color": "<fg #FC5F39>"},
|
||||
"WARNING": {"color": "#DC5F00", "icon": "⚠️", "loguru_color": "<fg #DC5F00>"},
|
||||
"ERROR": {"color": "#ff0000", "icon": "❌", "loguru_color": "<fg #ff0000>"},
|
||||
"CRITICAL": {"color": "#ff0000", "icon": "💀", "loguru_color": "<fg #ff0000>"},
|
||||
}
|
||||
|
||||
CUSTOM_LOG_LEVELS = {
|
||||
"COMET": {
|
||||
"color": "#7871d6",
|
||||
"icon": "🌠",
|
||||
"loguru_color": "<fg #7871d6>",
|
||||
"no": 50,
|
||||
},
|
||||
"API": {"color": "#006989", "icon": "👾", "loguru_color": "<fg #006989>", "no": 45},
|
||||
"SCRAPER": {
|
||||
"color": "#d6bb71",
|
||||
"icon": "👻",
|
||||
"loguru_color": "<fg #d6bb71>",
|
||||
"no": 40,
|
||||
},
|
||||
"STREAM": {
|
||||
"color": "#d171d6",
|
||||
"icon": "🎬",
|
||||
"loguru_color": "<fg #d171d6>",
|
||||
"no": 35,
|
||||
},
|
||||
"LOCK": {
|
||||
"color": "#71d6d6",
|
||||
"icon": "🔒",
|
||||
"loguru_color": "<fg #71d6d6>",
|
||||
"no": 30,
|
||||
},
|
||||
}
|
||||
|
||||
ALL_LOG_LEVELS = {**STANDARD_LOG_LEVELS, **CUSTOM_LOG_LEVELS}
|
||||
|
||||
|
||||
def get_level_info(level_name: str) -> dict:
|
||||
return ALL_LOG_LEVELS.get(level_name, {"color": "#ffffff", "icon": "📝"})
|
||||
|
||||
|
||||
def get_level_color(level_name: str) -> str:
|
||||
return get_level_info(level_name)["color"]
|
||||
|
||||
|
||||
def get_level_icon(level_name: str) -> str:
|
||||
return get_level_info(level_name)["icon"]
|
||||
+14
-8
@@ -2,6 +2,7 @@ import sys
|
||||
import logging
|
||||
|
||||
from loguru import logger
|
||||
from comet.utils.log_levels import CUSTOM_LOG_LEVELS, STANDARD_LOG_LEVELS
|
||||
|
||||
logging.getLogger("demagnetize").setLevel(
|
||||
logging.CRITICAL
|
||||
@@ -9,15 +10,20 @@ logging.getLogger("demagnetize").setLevel(
|
||||
|
||||
|
||||
def setupLogger(level: str):
|
||||
logger.level("COMET", no=50, icon="🌠", color="<fg #7871d6>")
|
||||
logger.level("API", no=45, icon="👾", color="<fg #006989>")
|
||||
logger.level("SCRAPER", no=40, icon="👻", color="<fg #d6bb71>")
|
||||
logger.level("STREAM", no=35, icon="🎬", color="<fg #d171d6>")
|
||||
logger.level("LOCK", no=30, icon="🔒", color="<fg #71d6d6>")
|
||||
# Configure custom log levels
|
||||
for level_name, level_config in CUSTOM_LOG_LEVELS.items():
|
||||
logger.level(
|
||||
level_name,
|
||||
no=level_config["no"],
|
||||
icon=level_config["icon"],
|
||||
color=level_config["loguru_color"],
|
||||
)
|
||||
|
||||
logger.level("INFO", icon="📰", color="<fg #FC5F39>")
|
||||
logger.level("DEBUG", icon="🕸️", color="<fg #DC5F00>")
|
||||
logger.level("WARNING", icon="⚠️", color="<fg #DC5F00>")
|
||||
# Configure standard log levels (override defaults)
|
||||
for level_name, level_config in STANDARD_LOG_LEVELS.items():
|
||||
logger.level(
|
||||
level_name, icon=level_config["icon"], color=level_config["loguru_color"]
|
||||
)
|
||||
|
||||
log_format = (
|
||||
"<white>{time:YYYY-MM-DD}</white> <magenta>{time:HH:mm:ss}</magenta> | "
|
||||
|
||||
@@ -32,7 +32,7 @@ class AppSettings(BaseSettings):
|
||||
FASTAPI_PORT: Optional[int] = 8000
|
||||
FASTAPI_WORKERS: Optional[int] = 1
|
||||
USE_GUNICORN: Optional[bool] = True
|
||||
DASHBOARD_ADMIN_PASSWORD: Optional[str] = "".join(
|
||||
ADMIN_DASHBOARD_PASSWORD: Optional[str] = "".join(
|
||||
random.choices(string.ascii_letters + string.digits, k=16)
|
||||
)
|
||||
DATABASE_TYPE: Optional[str] = "sqlite"
|
||||
|
||||
Reference in New Issue
Block a user