feat: implement new admin dashboard

This commit is contained in:
g0ldyy
2025-08-23 23:47:47 +02:00
parent 4df905e72a
commit dfe00a280c
9 changed files with 2449 additions and 966 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+725
View File
@@ -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>
&nbsp;Active Connections
</sl-tab>
<sl-tab slot="nav" panel="logs">
<sl-icon name="list-ul"></sl-icon>
&nbsp;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>
+333
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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
View File
@@ -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> | "
+1 -1
View File
@@ -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"