wip
This commit is contained in:
parent
82b2885576
commit
fd5840df94
5 changed files with 394 additions and 104 deletions
51
agent.py
51
agent.py
|
@ -25,6 +25,7 @@ SERVER_NAME = os.getenv('SERVER_NAME', socket.gethostname())
|
|||
API_KEY = os.getenv('API_KEY')
|
||||
CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '60'))
|
||||
VERIFY_SSL = os.getenv('VERIFY_SSL', 'true').lower() == 'true'
|
||||
SERVER_TYPE = os.getenv('SERVER_TYPE', 'caddy').lower() # 'caddy' or 'nginx'
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
|
@ -34,10 +35,11 @@ logging.basicConfig(
|
|||
logger = logging.getLogger('caddy-agent')
|
||||
|
||||
# Debug configuration
|
||||
logger.info(f"Starting Caddy agent with configuration:")
|
||||
logger.info(f"Starting {SERVER_TYPE} agent with configuration:")
|
||||
logger.info(f"- DASHBOARD_URL: {DASHBOARD_URL}")
|
||||
logger.info(f"- SERVER_NAME: {SERVER_NAME}")
|
||||
logger.info(f"- CADDYFILE_PATH: {CADDYFILE_PATH}")
|
||||
logger.info(f"- SERVER_TYPE: {SERVER_TYPE}")
|
||||
logger.info(f"- VERIFY_SSL: {VERIFY_SSL}")
|
||||
logger.info(f"- API_KEY set: {'Yes' if API_KEY else 'No'}")
|
||||
|
||||
|
@ -101,19 +103,21 @@ def parse_caddyfile():
|
|||
def create_auth_token():
|
||||
"""Create a JWT token for authentication"""
|
||||
if not API_KEY:
|
||||
logger.error("API_KEY is not set. Authentication will fail.")
|
||||
logger.error("Cannot create authentication token: API_KEY not set")
|
||||
return None
|
||||
|
||||
# Create a token that expires in 5 minutes
|
||||
# Create token with 5 minute expiry
|
||||
payload = {
|
||||
'server': SERVER_NAME,
|
||||
'exp': datetime.now() + timedelta(minutes=5)
|
||||
"exp": datetime.utcnow() + timedelta(minutes=5),
|
||||
"iat": datetime.utcnow(),
|
||||
"sub": SERVER_NAME
|
||||
}
|
||||
|
||||
try:
|
||||
return jwt.encode(payload, API_KEY, algorithm='HS256')
|
||||
token = jwt.encode(payload, API_KEY, algorithm="HS256")
|
||||
return token
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating authentication token: {e}")
|
||||
logger.error(f"Error creating JWT token: {e}")
|
||||
return None
|
||||
|
||||
def send_update(force=False):
|
||||
|
@ -134,7 +138,8 @@ def send_update(force=False):
|
|||
data = {
|
||||
"server": SERVER_NAME,
|
||||
"entries": current_data,
|
||||
"timestamp": current_time.isoformat()
|
||||
"timestamp": current_time.isoformat(),
|
||||
"type": SERVER_TYPE # Add the server type
|
||||
}
|
||||
|
||||
# Create authentication token
|
||||
|
@ -166,39 +171,33 @@ def send_update(force=False):
|
|||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Connection error sending update: {e}")
|
||||
|
||||
class CaddyfileHandler(FileSystemEventHandler):
|
||||
"""Watch for changes to the Caddyfile"""
|
||||
|
||||
class CaddyFileHandler(FileSystemEventHandler):
|
||||
"""Watch for changes to the Caddyfile and send updates"""
|
||||
def on_modified(self, event):
|
||||
if event.src_path == CADDYFILE_PATH:
|
||||
logger.info(f"Caddyfile modified: {event.src_path}")
|
||||
logger.info(f"Caddyfile changed, sending update")
|
||||
send_update()
|
||||
|
||||
def main():
|
||||
"""Main function to run the agent"""
|
||||
logger.info(f"Starting Caddy agent for {SERVER_NAME}")
|
||||
logger.info(f"Monitoring Caddyfile at: {CADDYFILE_PATH}")
|
||||
logger.info(f"Dashboard URL: {DASHBOARD_URL}")
|
||||
|
||||
# Initial update
|
||||
"""Main function to start the agent"""
|
||||
# Send initial update
|
||||
send_update(force=True)
|
||||
|
||||
# Setup file watching
|
||||
# Set up file watching
|
||||
event_handler = CaddyFileHandler()
|
||||
observer = Observer()
|
||||
event_handler = CaddyfileHandler()
|
||||
|
||||
# Watch the directory containing the Caddyfile
|
||||
caddyfile_dir = os.path.dirname(CADDYFILE_PATH)
|
||||
observer.schedule(event_handler, caddyfile_dir, recursive=False)
|
||||
observer.schedule(event_handler, path=os.path.dirname(CADDYFILE_PATH), recursive=False)
|
||||
observer.start()
|
||||
|
||||
try:
|
||||
logger.info(f"Agent started. Watching {CADDYFILE_PATH} for changes")
|
||||
while True:
|
||||
# Periodic check even if file doesn't change
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
# Send periodic updates
|
||||
send_update()
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
|
||||
observer.join()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
228
app.py
228
app.py
|
@ -5,7 +5,7 @@ import os
|
|||
import jwt
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from dotenv import load_dotenv
|
||||
import re
|
||||
import signal
|
||||
|
@ -18,6 +18,7 @@ load_dotenv()
|
|||
API_KEY = os.getenv('API_KEY')
|
||||
DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true'
|
||||
CADDYFILE_PATH = "/app/Caddyfile" # Fixed internal path
|
||||
NGINX_CONFIG_PATH = os.getenv('NGINX_CONFIG_PATH', '/app/nginx/conf.d') # Path to nginx config directory
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
|
@ -26,12 +27,16 @@ logging.basicConfig(
|
|||
)
|
||||
logger = logging.getLogger('caddy-dashboard')
|
||||
|
||||
# Determine if we should use local Caddyfile reading
|
||||
# Determine if we should use local configuration files
|
||||
USE_LOCAL_CADDYFILE = os.path.exists(CADDYFILE_PATH)
|
||||
USE_LOCAL_NGINX = os.path.exists(NGINX_CONFIG_PATH) and os.path.isdir(NGINX_CONFIG_PATH)
|
||||
|
||||
if not USE_LOCAL_CADDYFILE:
|
||||
logger.warning(f"Caddyfile not found at the standard path: {CADDYFILE_PATH}")
|
||||
|
||||
if not USE_LOCAL_NGINX:
|
||||
logger.warning(f"Nginx config directory not found at: {NGINX_CONFIG_PATH}")
|
||||
|
||||
if not API_KEY:
|
||||
logger.warning("API_KEY not set - running without authentication! This is insecure.")
|
||||
|
||||
|
@ -39,9 +44,9 @@ app = Flask(__name__)
|
|||
app.config['SECRET_KEY'] = os.urandom(24)
|
||||
|
||||
# Global data storage
|
||||
proxy_data = {}
|
||||
deleted_servers = set()
|
||||
server_last_seen = {} # Track when servers were last updated
|
||||
caddy_proxies = {} # Server name -> {domain: target}
|
||||
nginx_proxies = {} # Server name -> {domain: target}
|
||||
timestamps = {} # Server name -> timestamp
|
||||
|
||||
def verify_token(token):
|
||||
"""Verify the JWT token from an agent"""
|
||||
|
@ -94,77 +99,174 @@ def parse_local_caddyfile():
|
|||
|
||||
return entries
|
||||
|
||||
def parse_nginx_configs():
|
||||
"""Parse nginx config files to extract server_name and proxy_pass directives"""
|
||||
entries = {}
|
||||
|
||||
if not os.path.exists(NGINX_CONFIG_PATH) or not os.path.isdir(NGINX_CONFIG_PATH):
|
||||
logger.warning(f"Nginx config directory not found at: {NGINX_CONFIG_PATH}")
|
||||
return entries
|
||||
|
||||
try:
|
||||
# Get all .conf files in the nginx config directory
|
||||
for filename in os.listdir(NGINX_CONFIG_PATH):
|
||||
if not filename.endswith('.conf'):
|
||||
continue
|
||||
|
||||
filepath = os.path.join(NGINX_CONFIG_PATH, filename)
|
||||
with open(filepath, 'r') as file:
|
||||
content = file.read()
|
||||
|
||||
# Pattern to extract server_name and proxy_pass
|
||||
server_blocks = re.findall(r'server\s*{(.*?)}', content, re.DOTALL)
|
||||
|
||||
for block in server_blocks:
|
||||
# Extract server_name
|
||||
server_name_match = re.search(r'server_name\s+(.+?);', block)
|
||||
if not server_name_match:
|
||||
continue
|
||||
|
||||
server_names = server_name_match.group(1).split()
|
||||
|
||||
# Extract proxy_pass
|
||||
proxy_pass_match = re.search(r'proxy_pass\s+(https?://[^;]+);', block)
|
||||
if not proxy_pass_match:
|
||||
continue
|
||||
|
||||
proxy_target = proxy_pass_match.group(1)
|
||||
|
||||
# Add each server name with its proxy target
|
||||
for name in server_names:
|
||||
if name != '_' and name.lower() != 'host': # Skip default server and "host"
|
||||
entries[name] = proxy_target
|
||||
|
||||
logger.info(f"Extracted {len(entries)} domain entries from Nginx configs")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing Nginx configs: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
return entries
|
||||
|
||||
def validate_token(auth_header):
|
||||
"""Validate the JWT token from the Authorization header"""
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
return False
|
||||
|
||||
token = auth_header.split(" ")[1]
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, API_KEY, algorithms=["HS256"])
|
||||
return True
|
||||
except jwt.InvalidTokenError:
|
||||
return False
|
||||
|
||||
def get_server_stats():
|
||||
"""Get basic server statistics"""
|
||||
stats = {
|
||||
'caddy_servers': len(caddy_proxies),
|
||||
'nginx_servers': len(nginx_proxies),
|
||||
'total_domains': sum(len(entries) for entries in caddy_proxies.values()) +
|
||||
sum(len(entries) for entries in nginx_proxies.values()),
|
||||
'last_update': max(timestamps.values(), default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
}
|
||||
return stats
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Render the dashboard homepage"""
|
||||
data_to_display = {k: v for k, v in proxy_data.items() if k not in deleted_servers}
|
||||
|
||||
# Add local Caddyfile data if available
|
||||
if USE_LOCAL_CADDYFILE:
|
||||
local_data = parse_local_caddyfile()
|
||||
if local_data:
|
||||
data_to_display["Local Server"] = local_data
|
||||
|
||||
# Add last seen timestamps
|
||||
timestamps = {server: server_last_seen.get(server, "Never") for server in data_to_display}
|
||||
|
||||
return render_template('index.html', proxies=data_to_display, timestamps=timestamps)
|
||||
"""Dashboard home page"""
|
||||
stats = get_server_stats()
|
||||
return render_template('dashboard.html',
|
||||
caddy_count=stats['caddy_servers'],
|
||||
nginx_count=stats['nginx_servers'],
|
||||
domain_count=stats['total_domains'],
|
||||
last_update=stats['last_update'])
|
||||
|
||||
@app.route('/caddy')
|
||||
def caddy_dashboard():
|
||||
"""Caddy specific dashboard"""
|
||||
return render_template('index.html', proxies=caddy_proxies, timestamps=timestamps, server_type="Caddy")
|
||||
|
||||
@app.route('/nginx')
|
||||
def nginx_dashboard():
|
||||
"""Nginx specific dashboard"""
|
||||
return render_template('index.html', proxies=nginx_proxies, timestamps=timestamps, server_type="Nginx")
|
||||
|
||||
@app.route('/api/update', methods=['POST'])
|
||||
def update():
|
||||
"""Secure API endpoint for agents to update their data"""
|
||||
# Verify authentication
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
logger.warning("Missing or invalid Authorization header")
|
||||
return jsonify({"error": "Authentication required"}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
payload = verify_token(token)
|
||||
def update_proxies():
|
||||
"""API endpoint for agents to update proxy data"""
|
||||
# Verify token if API_KEY is set
|
||||
if API_KEY:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not validate_token(auth_header):
|
||||
return jsonify({"status": "error", "message": "Invalid or missing token"}), 401
|
||||
|
||||
if not payload:
|
||||
return jsonify({"error": "Invalid authentication token"}), 401
|
||||
|
||||
# Verify the server in the token matches the data
|
||||
# Get data from request
|
||||
data = request.json
|
||||
if not data or "server" not in data or "entries" not in data:
|
||||
return jsonify({"error": "Invalid data format"}), 400
|
||||
if not data or not isinstance(data, dict):
|
||||
return jsonify({"status": "error", "message": "Invalid data format"}), 400
|
||||
|
||||
server_name = data.get("server", "Local Server") # Default to 'Local Server' if not provided
|
||||
entries = data.get("entries", {})
|
||||
server_type = data.get("type", "caddy").lower() # Default to caddy if not specified
|
||||
|
||||
# Update the appropriate store based on server type
|
||||
if server_type == "nginx":
|
||||
nginx_proxies[server_name] = entries
|
||||
else:
|
||||
caddy_proxies[server_name] = entries
|
||||
|
||||
if payload.get('server') != data["server"]:
|
||||
logger.warning(f"Server mismatch: {payload.get('server')} vs {data['server']}")
|
||||
return jsonify({"error": "Server authentication mismatch"}), 403
|
||||
# Update timestamp
|
||||
timestamps[server_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Update the data
|
||||
server_name = data["server"]
|
||||
proxy_data[server_name] = data["entries"]
|
||||
server_last_seen[server_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# Log the update
|
||||
logger.info(f"Updated {server_type} proxy data for {server_name}: {len(entries)} entries")
|
||||
|
||||
if server_name in deleted_servers:
|
||||
deleted_servers.remove(server_name)
|
||||
|
||||
logger.info(f"Updated data for server: {server_name} with {len(data['entries'])} entries")
|
||||
return jsonify({"message": "Updated successfully"}), 200
|
||||
return jsonify({"status": "success", "message": f"Updated {len(entries)} entries for {server_name}"})
|
||||
|
||||
@app.route('/delete', methods=['POST'])
|
||||
def delete_entry():
|
||||
def delete_server():
|
||||
"""Delete a server from the dashboard"""
|
||||
data = request.json
|
||||
server_name = data.get("server")
|
||||
if not server_name or server_name not in proxy_data:
|
||||
return jsonify({"error": "Server not found"}), 400
|
||||
if not data or "server" not in data:
|
||||
return jsonify({"status": "error", "message": "Server name required"}), 400
|
||||
|
||||
server_name = data["server"]
|
||||
|
||||
# Remove from both stores and timestamps
|
||||
caddy_removed = server_name in caddy_proxies
|
||||
nginx_removed = server_name in nginx_proxies
|
||||
|
||||
if server_name in caddy_proxies:
|
||||
del caddy_proxies[server_name]
|
||||
|
||||
if server_name in nginx_proxies:
|
||||
del nginx_proxies[server_name]
|
||||
|
||||
if server_name in timestamps:
|
||||
del timestamps[server_name]
|
||||
|
||||
if not caddy_removed and not nginx_removed:
|
||||
return jsonify({"status": "error", "message": f"Server {server_name} not found"}), 404
|
||||
|
||||
logger.info(f"Deleted server: {server_name}")
|
||||
return jsonify({"status": "success", "message": f"Server {server_name} deleted"})
|
||||
|
||||
deleted_servers.add(server_name)
|
||||
logger.info(f"Server {server_name} marked as deleted")
|
||||
return jsonify({"message": f"Server {server_name} deleted"}), 200
|
||||
# Initialize with local files if available
|
||||
if USE_LOCAL_CADDYFILE:
|
||||
entries = parse_local_caddyfile()
|
||||
if entries:
|
||||
caddy_proxies["Local Server"] = entries
|
||||
timestamps["Local Server"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
logger.info(f"Loaded {len(entries)} entries from local Caddyfile")
|
||||
|
||||
@app.route('/status/<domain>')
|
||||
def check_status(domain):
|
||||
"""Check if a subdomain is reachable"""
|
||||
try:
|
||||
response = requests.get(f"https://{domain}", timeout=3)
|
||||
return jsonify({"status": response.status_code})
|
||||
except requests.exceptions.RequestException:
|
||||
return jsonify({"status": "offline"})
|
||||
if USE_LOCAL_NGINX:
|
||||
entries = parse_nginx_configs()
|
||||
if entries:
|
||||
nginx_proxies["Local Nginx"] = entries
|
||||
timestamps["Local Nginx"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
logger.info(f"Loaded {len(entries)} entries from local Nginx configs")
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
logger.info("Shutdown signal received, exiting gracefully...")
|
||||
|
@ -179,8 +281,8 @@ if __name__ == '__main__':
|
|||
# Load it initially
|
||||
local_data = parse_local_caddyfile()
|
||||
if local_data:
|
||||
proxy_data["Local Server"] = local_data
|
||||
server_last_seen["Local Server"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
caddy_proxies["Local Server"] = local_data
|
||||
timestamps["Local Server"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if not API_KEY:
|
||||
logger.warning("API_KEY not set - running without authentication!")
|
||||
|
|
39
compose.yml
39
compose.yml
|
@ -9,26 +9,49 @@ services:
|
|||
- DEBUG_MODE=false
|
||||
volumes:
|
||||
- ./Caddyfile:/app/Caddyfile:ro
|
||||
# - ./nginx:/app/nginx:ro # Mount nginx config directory
|
||||
command: server
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- caddy-network
|
||||
# networks:
|
||||
# - caddy-network
|
||||
|
||||
# Agent mode (example)
|
||||
caddydb-agent:
|
||||
# Caddy Agent mode
|
||||
caddydb-caddy-agent:
|
||||
image: caddydb:latest
|
||||
volumes:
|
||||
- /path/to/host/Caddyfile:/app/Caddyfile:ro
|
||||
- /path/to/your/Caddyfile:/app/Caddyfile:ro
|
||||
environment:
|
||||
- API_KEY=${API_KEY}
|
||||
- DASHBOARD_URL=http://caddydb-server:5000/api/update
|
||||
- SERVER_NAME=caddy-server-1
|
||||
- SERVER_TYPE=caddy
|
||||
- CHECK_INTERVAL=60
|
||||
- VERIFY_SSL=false # Set to false if using self-signed certificates
|
||||
- VERIFY_SSL=false
|
||||
command: agent
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- caddy-network
|
||||
# networks:
|
||||
# - caddy-network
|
||||
# depends_on:
|
||||
# - caddydb-server
|
||||
|
||||
# Nginx Agent mode (example)
|
||||
caddydb-nginx-agent:
|
||||
image: caddydb:latest
|
||||
volumes:
|
||||
- /path/to/your/nginx/conf.d:/app/nginx:ro
|
||||
environment:
|
||||
- API_KEY=${API_KEY}
|
||||
- DASHBOARD_URL=http://caddydb-server:5000/api/update
|
||||
- SERVER_NAME=nginx-server-1
|
||||
- SERVER_TYPE=nginx
|
||||
- CHECK_INTERVAL=60
|
||||
- VERIFY_SSL=false
|
||||
command: agent
|
||||
restart: unless-stopped
|
||||
# networks:
|
||||
# - caddy-network
|
||||
# depends_on:
|
||||
# - caddydb-server
|
||||
|
||||
networks:
|
||||
caddy-network:
|
||||
|
|
149
templates/dashboard.html
Normal file
149
templates/dashboard.html
Normal file
|
@ -0,0 +1,149 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Caddy Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #0f0f0f;
|
||||
background-image:
|
||||
radial-gradient(circle at 25% 25%, rgba(40, 40, 40, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 75% 75%, rgba(40, 40, 40, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
background-size: 50px 50px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(40, 40, 40, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(40, 40, 40, 0.05) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="text-gray-200 min-h-screen bg-grid">
|
||||
<!-- Minimal header -->
|
||||
<header class="py-4 px-6 bg-black/50 backdrop-blur-sm border-b border-gray-800/50">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<h1 class="text-xl font-medium tracking-tight text-white">Caddy Dashboard</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/caddy"
|
||||
class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md transition-colors">
|
||||
<i class="fas fa-server text-xs mr-1.5"></i>Caddy
|
||||
</a>
|
||||
<a href="/nginx"
|
||||
class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md transition-colors">
|
||||
<i class="fas fa-server text-xs mr-1.5"></i>Nginx
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto p-4 md:p-6">
|
||||
<!-- Overview Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 flex items-center justify-center bg-blue-500/10 text-blue-400 rounded-lg mr-4">
|
||||
<i class="fas fa-server text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Caddy Servers</p>
|
||||
<h3 class="text-xl font-medium text-white">{{ caddy_count }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 flex items-center justify-center bg-green-500/10 text-green-400 rounded-lg mr-4">
|
||||
<i class="fas fa-server text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Nginx Servers</p>
|
||||
<h3 class="text-xl font-medium text-white">{{ nginx_count }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 flex items-center justify-center bg-purple-500/10 text-purple-400 rounded-lg mr-4">
|
||||
<i class="fas fa-globe text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Total Domains</p>
|
||||
<h3 class="text-xl font-medium text-white">{{ domain_count }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 flex items-center justify-center bg-amber-500/10 text-amber-400 rounded-lg mr-4">
|
||||
<i class="fas fa-clock text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Last Update</p>
|
||||
<h3 class="text-sm font-medium text-white">{{ last_update }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Selection Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<a href="/caddy" class="card bg-zinc-900 border border-zinc-800 rounded-lg p-6 hover:border-blue-500/50">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-14 h-14 flex items-center justify-center bg-blue-500/10 text-blue-400 rounded-lg mr-5">
|
||||
<i class="fas fa-server text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-medium text-white">Caddy Server Dashboard</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">View all Caddy reverse proxy configurations</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<span class="text-xs text-gray-500">{{ caddy_count }} servers</span>
|
||||
<span class="text-blue-400 text-sm">View <i class="fas fa-arrow-right ml-1"></i></span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/nginx" class="card bg-zinc-900 border border-zinc-800 rounded-lg p-6 hover:border-green-500/50">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-14 h-14 flex items-center justify-center bg-green-500/10 text-green-400 rounded-lg mr-5">
|
||||
<i class="fas fa-server text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-medium text-white">Nginx Server Dashboard</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">View all Nginx proxy configurations</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<span class="text-xs text-gray-500">{{ nginx_count }} servers</span>
|
||||
<span class="text-green-400 text-sm">View <i class="fas fa-arrow-right ml-1"></i></span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="py-4 mt-12 border-t border-zinc-900">
|
||||
<div class="container mx-auto px-4 text-center text-xs text-gray-600">
|
||||
Caddy Dashboard • Supports both Caddy and Nginx configurations
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Caddy Dashboard</title>
|
||||
<title>{{ server_type }} Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
|
@ -98,12 +98,29 @@
|
|||
<!-- Minimal header -->
|
||||
<header class="py-4 px-6 bg-black/50 backdrop-blur-sm border-b border-gray-800/50">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<h1 class="text-xl font-medium tracking-tight text-white">Caddy Dashboard</h1>
|
||||
<button onclick="toggleSearch()" class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md
|
||||
transition-colors flex items-center gap-2">
|
||||
<i class="fas fa-search text-xs"></i>
|
||||
<span class="hidden sm:inline">/</span>
|
||||
</button>
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-gray-400 hover:text-white mr-3">
|
||||
<i class="fas fa-home"></i>
|
||||
</a>
|
||||
<h1 class="text-xl font-medium tracking-tight text-white">
|
||||
{{ server_type }} Dashboard
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/caddy"
|
||||
class="text-sm px-3 py-1.5 {% if server_type == 'Caddy' %}bg-blue-900/50 border-blue-700/50{% else %}bg-zinc-800 border-zinc-700{% endif %} hover:bg-zinc-700 border rounded-md transition-colors">
|
||||
<i class="fas fa-server text-xs mr-1.5"></i>Caddy
|
||||
</a>
|
||||
<a href="/nginx"
|
||||
class="text-sm px-3 py-1.5 {% if server_type == 'Nginx' %}bg-green-900/50 border-green-700/50{% else %}bg-zinc-800 border-zinc-700{% endif %} hover:bg-zinc-700 border rounded-md transition-colors">
|
||||
<i class="fas fa-server text-xs mr-1.5"></i>Nginx
|
||||
</a>
|
||||
<button onclick="toggleSearch()" class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md
|
||||
transition-colors flex items-center gap-2">
|
||||
<i class="fas fa-search text-xs"></i>
|
||||
<span class="hidden sm:inline">/</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue