wip
This commit is contained in:
parent
a4ce8a291d
commit
950d72aba1
8 changed files with 375 additions and 22 deletions
13
.env.example
Normal file
13
.env.example
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# API key for authentication between server and agents
|
||||||
|
API_KEY=your_secure_random_key_here
|
||||||
|
|
||||||
|
# Server mode configurations
|
||||||
|
DEBUG_MODE=false
|
||||||
|
CADDYFILE_PATH=/path/to/local/Caddyfile # Only needed in LOCAL_MODE
|
||||||
|
|
||||||
|
# Agent mode configurations
|
||||||
|
CADDYFILE_PATH=/opt/docker/caddy/conf/Caddyfile
|
||||||
|
DASHBOARD_URL=https://dashboard.example.com/api/update
|
||||||
|
# or DASHBOARD_URL=http://caddydb-server:5000/api/update
|
||||||
|
# SERVER_NAME=my-caddy-server # Optional - defaults to hostname
|
||||||
|
CHECK_INTERVAL=60 # Seconds between checks
|
|
@ -7,6 +7,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY . /app/
|
COPY . /app/
|
||||||
|
|
||||||
|
# Make entrypoint script executable
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
CMD ["python", "app.py"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["server"]
|
||||||
|
|
166
agent.py
Normal file
166
agent.py
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
import socket
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configuration (with environment variable support)
|
||||||
|
CADDYFILE_PATH = os.getenv('CADDYFILE_PATH', '/opt/docker/caddy/conf/Caddyfile')
|
||||||
|
DASHBOARD_URL = os.getenv('DASHBOARD_URL', 'https://dashboard.example.com/api/update')
|
||||||
|
SERVER_NAME = os.getenv('SERVER_NAME', socket.gethostname())
|
||||||
|
API_KEY = os.getenv('API_KEY') # Required for authentication
|
||||||
|
CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '60')) # Seconds between checks even if no file change
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('caddy-agent')
|
||||||
|
|
||||||
|
# Last data sent to avoid unnecessary updates
|
||||||
|
last_data_sent = None
|
||||||
|
last_send_time = datetime.min
|
||||||
|
|
||||||
|
def parse_caddyfile():
|
||||||
|
"""Parse the Caddyfile to extract domains and their proxy targets"""
|
||||||
|
entries = {}
|
||||||
|
try:
|
||||||
|
if not os.path.exists(CADDYFILE_PATH):
|
||||||
|
logger.error(f"Caddyfile not found at {CADDYFILE_PATH}")
|
||||||
|
return entries
|
||||||
|
|
||||||
|
with open(CADDYFILE_PATH, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
pattern = re.compile(r"(?P<domains>[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?P<target>https?:\/\/[\d\.]+:\d+|[\d\.]+:\d+).*?}", re.DOTALL)
|
||||||
|
matches = pattern.findall(content)
|
||||||
|
|
||||||
|
for domains, target in matches:
|
||||||
|
for domain in domains.split(", "):
|
||||||
|
domain = domain.strip()
|
||||||
|
if domain: # Only add non-empty domains
|
||||||
|
entries[domain] = target.strip()
|
||||||
|
|
||||||
|
logger.info(f"Found {len(entries)} domain entries in Caddyfile")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing Caddyfile: {e}")
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def create_auth_token():
|
||||||
|
"""Create a JWT token for authentication"""
|
||||||
|
if not API_KEY:
|
||||||
|
logger.error("API_KEY is not set. Authentication will fail.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create a token that expires in 5 minutes
|
||||||
|
payload = {
|
||||||
|
'server': SERVER_NAME,
|
||||||
|
'exp': datetime.utcnow() + timedelta(minutes=5)
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jwt.encode(payload, API_KEY, algorithm='HS256')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating authentication token: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_update(force=False):
|
||||||
|
"""Send Caddyfile data to the dashboard server"""
|
||||||
|
global last_data_sent, last_send_time
|
||||||
|
|
||||||
|
# Parse the Caddyfile
|
||||||
|
current_data = parse_caddyfile()
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
# Only send if data changed or enough time passed since last update
|
||||||
|
if (not force and
|
||||||
|
current_data == last_data_sent and
|
||||||
|
(current_time - last_send_time).total_seconds() < CHECK_INTERVAL):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create the data payload
|
||||||
|
data = {
|
||||||
|
"server": SERVER_NAME,
|
||||||
|
"entries": current_data,
|
||||||
|
"timestamp": current_time.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create authentication token
|
||||||
|
token = create_auth_token()
|
||||||
|
if not token:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prepare headers with authentication
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': f'Bearer {token}'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
DASHBOARD_URL,
|
||||||
|
json=data,
|
||||||
|
headers=headers,
|
||||||
|
timeout=10 # Set a reasonable timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info(f"Update sent successfully: {response.json()}")
|
||||||
|
last_data_sent = current_data
|
||||||
|
last_send_time = current_time
|
||||||
|
else:
|
||||||
|
logger.error(f"Error sending update: {response.status_code} - {response.text}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Connection error sending update: {e}")
|
||||||
|
|
||||||
|
class CaddyfileHandler(FileSystemEventHandler):
|
||||||
|
"""Watch for changes to the Caddyfile"""
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
if event.src_path == CADDYFILE_PATH:
|
||||||
|
logger.info(f"Caddyfile modified: {event.src_path}")
|
||||||
|
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
|
||||||
|
send_update(force=True)
|
||||||
|
|
||||||
|
# Setup file watching
|
||||||
|
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.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Periodic check even if file doesn't change
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
send_update()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
132
app.py
132
app.py
|
@ -1,39 +1,144 @@
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify, abort
|
||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
|
import os
|
||||||
|
import jwt
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
API_KEY = os.getenv('API_KEY') # Must match agent configuration
|
||||||
|
DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true'
|
||||||
|
CADDYFILE_PATH = os.getenv('CADDYFILE_PATH') # Optional - for direct file reading
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG if DEBUG_MODE else logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('caddy-dashboard')
|
||||||
|
|
||||||
|
# Determine if we should use local Caddyfile reading
|
||||||
|
USE_LOCAL_CADDYFILE = CADDYFILE_PATH and os.path.exists(CADDYFILE_PATH)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = os.urandom(24)
|
||||||
|
|
||||||
|
# Global data storage
|
||||||
proxy_data = {}
|
proxy_data = {}
|
||||||
deleted_servers = set()
|
deleted_servers = set()
|
||||||
|
server_last_seen = {} # Track when servers were last updated
|
||||||
|
|
||||||
|
def verify_token(token):
|
||||||
|
"""Verify the JWT token from an agent"""
|
||||||
|
if not API_KEY:
|
||||||
|
logger.error("API_KEY is not configured - authentication disabled")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jwt.decode(token, API_KEY, algorithms=['HS256'])
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
logger.warning("Authentication token has expired")
|
||||||
|
return None
|
||||||
|
except jwt.InvalidTokenError as e:
|
||||||
|
logger.warning(f"Invalid authentication token: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_local_caddyfile():
|
||||||
|
"""Parse a local Caddyfile if LOCAL_MODE is enabled"""
|
||||||
|
if not CADDYFILE_PATH or not os.path.exists(CADDYFILE_PATH):
|
||||||
|
logger.error(f"Local Caddyfile not found at {CADDYFILE_PATH}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
import re
|
||||||
|
|
||||||
|
entries = {}
|
||||||
|
try:
|
||||||
|
with open(CADDYFILE_PATH, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
pattern = re.compile(r"(?P<domains>[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?P<target>https?:\/\/[\d\.]+:\d+|[\d\.]+:\d+).*?}", re.DOTALL)
|
||||||
|
matches = pattern.findall(content)
|
||||||
|
|
||||||
|
for domains, target in matches:
|
||||||
|
for domain in domains.split(", "):
|
||||||
|
domain = domain.strip()
|
||||||
|
if domain:
|
||||||
|
entries[domain] = target.strip()
|
||||||
|
|
||||||
|
logger.info(f"Found {len(entries)} domain entries in local Caddyfile")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing local Caddyfile: {e}")
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
filtered_data = {k: v for k, v in proxy_data.items() if k not in deleted_servers}
|
"""Render the dashboard homepage"""
|
||||||
return render_template('index.html', proxies=filtered_data)
|
data_to_display = {k: v for k, v in proxy_data.items() if k not in deleted_servers}
|
||||||
|
|
||||||
@app.route('/update', methods=['POST'])
|
# 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)
|
||||||
|
|
||||||
|
@app.route('/api/update', methods=['POST'])
|
||||||
def update():
|
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)
|
||||||
|
|
||||||
|
if not payload:
|
||||||
|
return jsonify({"error": "Invalid authentication token"}), 401
|
||||||
|
|
||||||
|
# Verify the server in the token matches the data
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data or "server" not in data or "entries" not in data:
|
if not data or "server" not in data or "entries" not in data:
|
||||||
return jsonify({"error": "Invalid data"}), 400
|
return jsonify({"error": "Invalid data format"}), 400
|
||||||
|
|
||||||
|
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 the data
|
||||||
server_name = data["server"]
|
server_name = data["server"]
|
||||||
proxy_data[server_name] = data["entries"]
|
proxy_data[server_name] = data["entries"]
|
||||||
|
server_last_seen[server_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
if server_name in deleted_servers:
|
if server_name in deleted_servers:
|
||||||
deleted_servers.remove(server_name)
|
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({"message": "Updated successfully"}), 200
|
||||||
|
|
||||||
@app.route('/delete', methods=['POST'])
|
@app.route('/delete', methods=['POST'])
|
||||||
def delete_entry():
|
def delete_entry():
|
||||||
|
"""Delete a server from the dashboard"""
|
||||||
data = request.json
|
data = request.json
|
||||||
server_name = data.get("server")
|
server_name = data.get("server")
|
||||||
if not server_name or server_name not in proxy_data:
|
if not server_name or server_name not in proxy_data:
|
||||||
return jsonify({"error": "Server not found"}), 400
|
return jsonify({"error": "Server not found"}), 400
|
||||||
|
|
||||||
deleted_servers.add(server_name)
|
deleted_servers.add(server_name)
|
||||||
|
logger.info(f"Server {server_name} marked as deleted")
|
||||||
return jsonify({"message": f"Server {server_name} deleted"}), 200
|
return jsonify({"message": f"Server {server_name} deleted"}), 200
|
||||||
|
|
||||||
@app.route('/status/<domain>')
|
@app.route('/status/<domain>')
|
||||||
|
@ -46,4 +151,19 @@ def check_status(domain):
|
||||||
return jsonify({"status": "offline"})
|
return jsonify({"status": "offline"})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000)
|
if USE_LOCAL_CADDYFILE:
|
||||||
|
logger.info(f"Local Caddyfile found at {CADDYFILE_PATH} - will display its data")
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
if not API_KEY:
|
||||||
|
logger.warning("API_KEY not set - running without authentication!")
|
||||||
|
|
||||||
|
# Use HTTPS in production
|
||||||
|
if DEBUG_MODE:
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
|
else:
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
|
|
25
compose.yml
25
compose.yml
|
@ -1,5 +1,26 @@
|
||||||
services:
|
services:
|
||||||
caddydb:
|
# Server mode
|
||||||
|
caddydb-server:
|
||||||
image: caddydb:latest
|
image: caddydb:latest
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- API_KEY=${API_KEY}
|
||||||
|
- DEBUG_MODE=false
|
||||||
|
volumes:
|
||||||
|
- ./.env:/app/.env:ro # Mount .env file
|
||||||
|
command: server
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Agent mode (example)
|
||||||
|
caddydb-agent:
|
||||||
|
image: caddydb:latest
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/caddy/conf/Caddyfile:/opt/docker/caddy/conf/Caddyfile:ro
|
||||||
|
- ./.env:/app/.env:ro # Mount .env file
|
||||||
|
environment:
|
||||||
|
- API_KEY=${API_KEY}
|
||||||
|
- DASHBOARD_URL=http://caddydb-server:5000/api/update
|
||||||
|
- SERVER_NAME=caddy-server-1
|
||||||
|
command: agent
|
||||||
|
restart: unless-stopped
|
||||||
|
|
13
entrypoint.sh
Normal file
13
entrypoint.sh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$1" = "server" ]; then
|
||||||
|
echo "Starting in server mode..."
|
||||||
|
exec python app.py
|
||||||
|
elif [ "$1" = "agent" ]; then
|
||||||
|
echo "Starting in agent mode..."
|
||||||
|
exec python agent.py
|
||||||
|
else
|
||||||
|
echo "Unknown mode: $1"
|
||||||
|
echo "Usage: $0 [server|agent]"
|
||||||
|
exit 1
|
||||||
|
fi
|
|
@ -1,2 +1,6 @@
|
||||||
flask
|
flask
|
||||||
requests
|
requests
|
||||||
|
pyjwt
|
||||||
|
cryptography
|
||||||
|
watchdog
|
||||||
|
python-dotenv
|
|
@ -1,5 +1,6 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
@ -26,8 +27,8 @@
|
||||||
function deleteServer(serverName) {
|
function deleteServer(serverName) {
|
||||||
fetch('/delete', {
|
fetch('/delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({"server": serverName})
|
body: JSON.stringify({ "server": serverName })
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
document.getElementById("server-box-" + serverName).remove();
|
document.getElementById("server-box-" + serverName).remove();
|
||||||
|
@ -35,7 +36,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", function(event) {
|
document.addEventListener("keydown", function (event) {
|
||||||
if (event.key === "/") {
|
if (event.key === "/") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
toggleSearch();
|
toggleSearch();
|
||||||
|
@ -43,29 +44,39 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-900 text-gray-100">
|
<body class="bg-gray-900 text-gray-100">
|
||||||
|
|
||||||
<header class="bg-indigo-700 text-white p-6 text-center">
|
<header class="bg-indigo-700 text-white p-6 text-center">
|
||||||
<h1 class="text-3xl font-bold">Caddy Dashboard</h1>
|
<h1 class="text-3xl font-bold">Caddy Dashboard</h1>
|
||||||
<p class="text-cyan-300 text-lg">Übersicht über aller aktiven Proxy-Server</p>
|
<p class="text-cyan-300 text-lg">Übersicht über aller aktiven Proxy-Server</p>
|
||||||
<button onclick="toggleSearch()" class="bg-cyan-500 hover:bg-cyan-400 text-white text-right px-6 py-3 rounded-lg mt-4 text-lg">🔍 Suche</button>
|
<button onclick="toggleSearch()"
|
||||||
|
class="bg-cyan-500 hover:bg-cyan-400 text-white text-right px-6 py-3 rounded-lg mt-4 text-lg">🔍
|
||||||
|
Suche</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container mx-auto p-4 md:p-6">
|
<div class="container mx-auto p-4 md:p-6">
|
||||||
<input type="text" id="search-box" class="hidden w-full p-4 mb-4 text-gray-900 text-lg rounded-md" placeholder="🔍 Suche nach Subdomains.." onkeyup="filterEntries()">
|
<input type="text" id="search-box" class="hidden w-full p-4 mb-4 text-gray-900 text-lg rounded-md"
|
||||||
|
placeholder="🔍 Suche nach Subdomains.." onkeyup="filterEntries()">
|
||||||
|
|
||||||
{% for server, entries in proxies.items() %}
|
{% for server, entries in proxies.items() %}
|
||||||
<div id="server-box-{{ server }}" class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
|
<div id="server-box-{{ server }}" class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
|
||||||
<h2 class="text-2xl font-semibold text-indigo-400">{{ server }}</h2>
|
<div>
|
||||||
<button onclick="deleteServer('{{ server }}')" class="mt-3 md:mt-0 bg-red-500 text-white px-6 py-2 rounded-lg text-lg">🗑️ Löschen</button>
|
<h2 class="text-2xl font-semibold text-indigo-400">{{ server }}</h2>
|
||||||
|
<p class="text-sm text-gray-400">Zuletzt aktualisiert: {{ timestamps[server] }}</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="deleteServer('{{ server }}')"
|
||||||
|
class="mt-3 md:mt-0 bg-red-500 text-white px-6 py-2 rounded-lg text-lg">🗑️ Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
||||||
{% for domain, target in entries.items() %}
|
{% for domain, target in entries.items() %}
|
||||||
<div class="domain-card bg-gray-700 p-4 rounded-lg shadow-md flex flex-col space-y-2" data-domain="{{ domain }}">
|
<div class="domain-card bg-gray-700 p-4 rounded-lg shadow-md flex flex-col space-y-2"
|
||||||
|
data-domain="{{ domain }}">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<a href="https://{{ domain }}" target="_blank" class="text-indigo-400 hover:text-indigo-300 text-lg break-words">{{ domain }}</a>
|
<a href="https://{{ domain }}" target="_blank"
|
||||||
|
class="text-indigo-400 hover:text-indigo-300 text-lg break-words">{{ domain }}</a>
|
||||||
<!-- <button onclick="checkStatus('{{ domain }}', this)" class="bg-cyan-500 text-white px-4 py-2 rounded-lg text-sm">🔄 Prüfen</button> -->
|
<!-- <button onclick="checkStatus('{{ domain }}', this)" class="bg-cyan-500 text-white px-4 py-2 rounded-lg text-sm">🔄 Prüfen</button> -->
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 break-words text-lg">{{ target }}</p>
|
<p class="text-gray-300 break-words text-lg">{{ target }}</p>
|
||||||
|
@ -77,4 +88,5 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue