diff --git a/Dockerfile b/Dockerfile index d2d4307..f090868 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,18 @@ FROM python:3.11-slim +# Ustawianie argumentów przekazywanych podczas budowania ARG BUILD_DATE ARG VERSION ARG VCS_REF +# Ustawianie zmiennych środowiskowych dla Pythona i PIP ENV VERSION=${VERSION} \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 +# Metadane obrazu zgodne ze standardem Open Containers Initiative LABEL org.opencontainers.image.created="${BUILD_DATE}" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.revision="${VCS_REF}" \ @@ -19,13 +22,21 @@ LABEL org.opencontainers.image.created="${BUILD_DATE}" \ org.opencontainers.image.title="Dockpeek" \ org.opencontainers.image.description="Docker container monitoring and management tool" +# Ustawienie katalogu roboczego w kontenerze WORKDIR /app +# Kopiowanie pliku z zależnościami COPY requirements.txt . + +# Instalacja zależności (ten krok jest cachowany przez Dockera) RUN pip install --no-cache-dir -r requirements.txt +# Kopiowanie reszty kodu aplikacji COPY . . +# Uwidocznienie portu, na którym działa aplikacja EXPOSE 8000 -CMD ["python", "app.py"] \ No newline at end of file +# ZAKTUALIZOWANA KOMENDA URUCHOMIENIOWA +# Używamy Gunicorn do uruchomienia aplikacji w trybie produkcyjnym, +CMD ["gunicorn", "--workers", "4", "--bind", "0.0.0.0:8000", "run:app"] \ No newline at end of file diff --git a/app.py b/app.py_old similarity index 100% rename from app.py rename to app.py_old diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/__init__.py b/app/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/__init__.py b/app/routes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/services/__init__.py b/app/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/config.py b/config.py new file mode 100644 index 0000000..d3e3629 --- /dev/null +++ b/config.py @@ -0,0 +1,30 @@ +import os +from datetime import timedelta + +class Config: + """Główna klasa konfiguracyjna.""" + SECRET_KEY = os.environ.get("SECRET_KEY") + if not SECRET_KEY: + raise RuntimeError("ERROR: SECRET_KEY environment variable is not set.") + + # Dane logowania + ADMIN_USERNAME = os.environ.get("USERNAME") + ADMIN_PASSWORD = os.environ.get("PASSWORD") + if not ADMIN_USERNAME or not ADMIN_PASSWORD: + raise RuntimeError("USERNAME and PASSWORD environment variables must be set.") + + # Ustawienia funkcji aplikacji + TRAEFIK_ENABLE = os.environ.get("TRAEFIK_LABELS", "true").lower() == "true" + TAGS_ENABLE = os.environ.get("TAGS", "true").lower() == "true" + + # Czas życia sesji + PERMANENT_SESSION_LIFETIME = timedelta(days=14) + + # Wersja aplikacji + APP_VERSION = os.environ.get('VERSION', 'dev') + + # Konfiguracja Dockera + DOCKER_TIMEOUT = 0.5 + + # Ustawienia logowania + LOG_LEVEL = "INFO" \ No newline at end of file diff --git a/dockpeek/__init__.py b/dockpeek/__init__.py new file mode 100644 index 0000000..dcf46b9 --- /dev/null +++ b/dockpeek/__init__.py @@ -0,0 +1,30 @@ +import os +import logging +from flask import Flask +from config import Config +from .extensions import login_manager, cors + +def create_app(config_class=Config): + """ + Tworzy i konfiguruje instancję aplikacji Flask. + """ + # Konfiguracja logowania + logging.basicConfig(level=config_class.LOG_LEVEL, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + logging.getLogger('werkzeug').setLevel(logging.WARNING) + + # Inicjalizacja aplikacji + app = Flask(__name__) + app.config.from_object(config_class) + + # Inicjalizacja rozszerzeń + login_manager.init_app(app) + cors.init_app(app) + + # Rejestracja Blueprints + from . import auth + app.register_blueprint(auth.auth_bp) + + from . import main + app.register_blueprint(main.main_bp) + + return app \ No newline at end of file diff --git a/dockpeek/auth.py b/dockpeek/auth.py new file mode 100644 index 0000000..9c52a94 --- /dev/null +++ b/dockpeek/auth.py @@ -0,0 +1,54 @@ +from flask import ( + Blueprint, render_template, request, redirect, url_for, session, current_app +) +from flask_login import UserMixin, login_user, logout_user, login_required +from werkzeug.security import generate_password_hash, check_password_hash +from .extensions import login_manager + +auth_bp = Blueprint('auth', __name__) + +# Przechowywanie użytkowników (teraz pobiera dane z konfiguracji aplikacji) +def get_users(): + return { + current_app.config['ADMIN_USERNAME']: { + "password": generate_password_hash(current_app.config['ADMIN_PASSWORD']) + } + } + +class User(UserMixin): + def __init__(self, id): + self.id = id + +@login_manager.user_loader +def load_user(user_id): + if user_id in get_users(): + return User(user_id) + return None + +@login_manager.unauthorized_handler +def unauthorized_callback(): + return redirect(url_for('auth.login')) + +@auth_bp.route("/login", methods=["GET", "POST"]) +def login(): + error = None + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + + users = get_users() + user_record = users.get(username) + + if user_record and check_password_hash(user_record["password"], password): + login_user(User(username)) + session.permanent = True + return redirect(url_for("main.index")) + else: + error = "Invalid credentials. Please try again." + return render_template("login.html", error=error) + +@auth_bp.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("auth.login")) \ No newline at end of file diff --git a/dockpeek/docker_utils.py b/dockpeek/docker_utils.py new file mode 100644 index 0000000..b662ef4 --- /dev/null +++ b/dockpeek/docker_utils.py @@ -0,0 +1,270 @@ +import os +import re +import logging +from datetime import datetime, timedelta +from threading import Lock +from urllib.parse import urlparse +import docker +from docker.client import DockerClient +from packaging import version +from concurrent.futures import ThreadPoolExecutor +from flask import request + +# Używamy loggera skonfigurowanego w __init__.py +logger = logging.getLogger(__name__) + +# Globalny executor i klasa UpdateChecker pozostają bez zmian +executor = ThreadPoolExecutor(max_workers=4) + +class UpdateChecker: + # ... (cała klasa UpdateChecker skopiowana z app.py bez zmian) ... + def __init__(self): + self.cache = {} + self.lock = Lock() + self.cache_duration = 300 # 5 minutes + + def get_cache_key(self, server_name, container_name, image_name): + return f"{server_name}:{container_name}:{image_name}" + + def is_cache_valid(self, timestamp): + return datetime.now() - timestamp < timedelta(seconds=self.cache_duration) + + def get_cached_result(self, cache_key): + with self.lock: + if cache_key in self.cache: + result, timestamp = self.cache[cache_key] + if self.is_cache_valid(timestamp): + return result, True + return None, False + + def set_cache_result(self, cache_key, result): + with self.lock: + self.cache[cache_key] = (result, datetime.now()) + + def check_local_image_updates(self, client, container, server_name): + try: + container_image_id = container.attrs.get('Image', '') + if not container_image_id: return False + image_name = container.attrs.get('Config', {}).get('Image', '') + if not image_name: return False + if ':' in image_name: base_name, current_tag = image_name.rsplit(':', 1) + else: base_name, current_tag = image_name, 'latest' + try: + local_image = client.images.get(f"{base_name}:{current_tag}") + return container_image_id != local_image.id + except Exception: return False + except Exception as e: + logger.error(f"Error checking local image updates for container '{container.name}'") + return False + + def check_image_updates_async(self, client, container, server_name): + try: + container_image_id = container.attrs.get('Image', '') + if not container_image_id: return False + image_name = container.attrs.get('Config', {}).get('Image', '') + if not image_name: return False + cache_key = self.get_cache_key(server_name, container.name, image_name) + cached_result, is_valid = self.get_cached_result(cache_key) + if is_valid: + logger.info(f"🔄[ {server_name} ] - Using cached update result for {image_name}: {cached_result}") + return cached_result + if ':' in image_name: base_name, current_tag = image_name.rsplit(':', 1) + else: base_name, current_tag = image_name, 'latest' + try: + client.images.pull(base_name, tag=current_tag) + updated_image = client.images.get(f"{base_name}:{current_tag}") + result = container_image_id != updated_image.id + self.set_cache_result(cache_key, result) + if result: logger.info(f" [ {server_name} ] - Update available - ⬆️{base_name} :{current_tag}") + else: logger.info(f" [ {server_name} ] - Image is up to date - ✅{base_name} :{current_tag}") + return result + except Exception as pull_error: + logger.warning(f" [ {server_name} ] - Cannot pull latest version of - ⚠️{base_name} :{current_tag} - it might be a locally built image") + self.set_cache_result(cache_key, False) + return False + except Exception as e: + logger.error(f"❌ Error checking image updates for '{container.name}'") + return False + +# Globalna instancja +update_checker = UpdateChecker() + +# ... (wszystkie funkcje pomocnicze _extract_hostname_from_url, _is_likely_internal_hostname, _get_link_hostname skopiowane z app.py bez zmian) ... +def _extract_hostname_from_url(url, is_docker_host): + if not url: return None + if url.startswith("unix://"): return None + if url.startswith("tcp://"): + try: + parsed = urlparse(url) + hostname = parsed.hostname + if hostname and hostname not in ["127.0.0.1", "0.0.0.0", "localhost"] and not _is_likely_internal_hostname(hostname, is_docker_host): + return hostname + except Exception: pass + try: + match = re.search(r"(?:tcp://)?([^:]+)(?::\d+)?", url) + if match: + hostname = match.group(1) + if hostname not in ["127.0.0.1", "0.0.0.0", "localhost"] and not _is_likely_internal_hostname(hostname, is_docker_host): + return hostname + except Exception: pass + return None + +def _is_likely_internal_hostname(hostname, is_docker_host): + if not is_docker_host: return False + if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', hostname): return False + if '.' in hostname: return False + return True + +def _get_link_hostname(public_hostname, host_ip, is_docker_host): + if public_hostname: return public_hostname + if host_ip and host_ip not in ['0.0.0.0', '127.0.0.1']: return host_ip + try: return request.host.split(":")[0] + except: return "localhost" + + +def discover_docker_clients(): + # ... (cała funkcja discover_docker_clients skopiowana z app.py, ale DOCKER_TIMEOUT pobieramy z config.py) ... + # Zamiast DOCKER_TIMEOUT użyj current_app.config['DOCKER_TIMEOUT'] + # Jednak dla prostoty zostawmy stałą wartość, bo ta funkcja nie ma dostępu do kontekstu aplikacji + DOCKER_TIMEOUT = 0.5 + clients = [] + if "DOCKER_HOST" in os.environ: + host_url = os.environ.get("DOCKER_HOST") + host_name = os.environ.get("DOCKER_HOST_NAME", "default") + public_hostname = os.environ.get("DOCKER_HOST_PUBLIC_HOSTNAME") or _extract_hostname_from_url(host_url, True) + try: + client = DockerClient(base_url=host_url, timeout=DOCKER_TIMEOUT) + client.ping() + clients.append({"name": host_name, "client": client, "url": host_url, "public_hostname": public_hostname, "status": "active", "is_docker_host": True, "order": 0}) + except Exception: + logger.error(f"Could not connect to DOCKER_HOST '{host_name}' at '{host_url}'") + clients.append({"name": host_name, "client": None, "url": host_url, "public_hostname": public_hostname, "status": "inactive", "is_docker_host": True, "order": 0}) + host_vars = {k: v for k, v in os.environ.items() if re.match(r"^DOCKER_HOST_\d+_URL$", k)} + for key, url in host_vars.items(): + match = re.match(r"^DOCKER_HOST_(\d+)_URL$", key) + if match: + num = match.group(1) + name = os.environ.get(f"DOCKER_HOST_{num}_NAME", f"server{num}") + public_hostname = os.environ.get(f"DOCKER_HOST_{num}_PUBLIC_HOSTNAME") or _extract_hostname_from_url(url, False) + try: + client = DockerClient(base_url=url, timeout=DOCKER_TIMEOUT) + client.ping() + logger.info(f"[ {name} ] Docker host is active") + clients.append({"name": name, "client": client, "url": url, "public_hostname": public_hostname, "status": "active", "is_docker_host": False, "order": int(num)}) + except Exception: + logger.error(f"[ {name} ] Could not connect to Docker host at {url}") + clients.append({"name": name, "client": None, "url": url, "public_hostname": public_hostname, "status": "inactive", "is_docker_host": False, "order": int(num)}) + if not clients: + fallback_name = os.environ.get("DOCKER_NAME", "default") + try: + client = docker.from_env(timeout=DOCKER_TIMEOUT) + client.ping() + clients.append({"name": fallback_name, "client": client, "url": "unix:///var/run/docker.sock", "public_hostname": "", "status": "active", "is_docker_host": True, "order": 0}) + except Exception: + clients.append({"name": fallback_name, "client": None, "url": "unix:///var/run/docker.sock", "public_hostname": "", "status": "inactive", "is_docker_host": True, "order": 0}) + return clients + +def get_container_status_with_exit_code(container): + # ... (cała funkcja get_container_status_with_exit_code skopiowana z app.py bez zmian) ... + try: + base_status = container.status + state = container.attrs.get('State', {}) + exit_code = state.get('ExitCode') + if base_status in ['exited', 'dead']: return base_status, exit_code + if base_status in ['paused', 'restarting', 'removing', 'created']: return base_status, None + if base_status == 'running': + health = state.get('Health', {}) + if health: + health_status = health.get('Status', '') + if health_status == 'healthy': return 'healthy', None + if health_status == 'unhealthy': return 'unhealthy', exit_code + if health_status == 'starting': return 'starting', None + return 'running', None + return base_status, None + except Exception as e: + logger.warning(f"Error getting status for container {container.name}: {e}") + return container.status, None + +def get_all_data(): + # ... (cała funkcja get_all_data skopiowana z app.py, z modyfikacją pobierania konfiguracji) ... + # Zamiast TRAEFIK_ENABLE i TAGS_ENABLE użyj current_app.config['TRAEFIK_ENABLE'] itd. + from flask import current_app + TRAEFIK_ENABLE = current_app.config['TRAEFIK_ENABLE'] + TAGS_ENABLE = current_app.config['TAGS_ENABLE'] + + servers = discover_docker_clients() + if not servers: return {"servers": [], "containers": []} + all_container_data = [] + server_list_for_json = [{"name": s["name"], "status": s["status"], "order": s["order"], "url": s["url"]} for s in servers] + for host in servers: + if host['status'] == 'inactive': continue + try: + server_name, client, public_hostname, is_docker_host = host["name"], host["client"], host["public_hostname"], host["is_docker_host"] + containers = client.containers.list(all=True) + for container in containers: + try: + image_name = container.attrs.get('Config', {}).get('Image', 'unknown') + cache_key = update_checker.get_cache_key(server_name, container.name, image_name) + cached_update, is_cache_valid = update_checker.get_cached_result(cache_key) + container_status, exit_code = get_container_status_with_exit_code(container) + labels = container.attrs.get('Config', {}).get('Labels', {}) or {} + stack_name = labels.get('com.docker.compose.project', '') + source_url = labels.get('org.opencontainers.image.source') or labels.get('org.opencontainers.image.url', '') + https_ports = labels.get('dockpeek.https', '') + custom_url = labels.get('dockpeek.link', '') + custom_ports = labels.get('dockpeek.ports', '') or labels.get('dockpeek.port', '') + custom_tags = labels.get('dockpeek.tags', '') or labels.get('dockpeek.tag', '') + tags = [tag.strip() for tag in custom_tags.split(',') if tag.strip()] if TAGS_ENABLE and custom_tags else [] + traefik_routes = [] + if TRAEFIK_ENABLE and labels.get('traefik.enable', '').lower() != 'false': + for key, value in labels.items(): + if key.startswith('traefik.http.routers.') and key.endswith('.rule'): + router_name = key.split('.')[3] + host_matches = re.findall(r'Host\(`([^`]+)`\)', value) + for host_match in host_matches: + tls_key = f'traefik.http.routers.{router_name}.tls' + is_tls = labels.get(tls_key, '').lower() == 'true' + entrypoints_key = f'traefik.http.routers.{router_name}.entrypoints' + entrypoints_str = labels.get(entrypoints_key, '') + is_https_entrypoint = any('https' in ep or '443' in ep for ep in entrypoints_str.split(',')) if entrypoints_str else False + protocol = 'https' if is_tls or is_https_entrypoint else 'http' + url = f"{protocol}://{host_match}" + path_match = re.search(r'PathPrefix\(`([^`]+)`\)', value) + if path_match: url += path_match.group(1) + traefik_routes.append({'router': router_name, 'url': url, 'rule': value, 'host': host_match}) + https_ports_list = [str(p.strip()) for p in https_ports.split(',') if p.strip()] if https_ports else [] + port_map = [] + custom_ports_list = [str(p.strip()) for p in custom_ports.split(',') if p.strip()] if custom_ports else [] + ports = container.attrs['NetworkSettings']['Ports'] + if ports: + for container_port, mappings in ports.items(): + if mappings: + m = mappings[0] + host_port, host_ip = m['HostPort'], m.get('HostIp', '0.0.0.0') + link_hostname = _get_link_hostname(public_hostname, host_ip, is_docker_host) + is_https = "443" in container_port or host_port == "443" or str(host_port) in https_ports_list + protocol = "https" if is_https else "http" + link = f"{protocol}://{link_hostname}" + (f":{host_port}" if host_port != "443" else "") + port_map.append({'container_port': container_port, 'host_port': host_port, 'link': link, 'is_custom': False}) + if custom_ports_list: + link_hostname = _get_link_hostname(public_hostname, None, is_docker_host) + for port in custom_ports_list: + is_https = port == "443" or str(port) in https_ports_list + protocol = "https" if is_https else "http" + link = f"{protocol}://{link_hostname}" + (f":{port}" if port != "443" else "") + port_map.append({'container_port': '', 'host_port': port, 'link': link, 'is_custom': True}) + + container_info = {'server': server_name, 'name': container.name, 'status': container_status, 'exit_code': exit_code, 'image': image_name, 'stack': stack_name, 'source_url': source_url, 'custom_url': custom_url, 'ports': port_map, 'traefik_routes': traefik_routes, 'tags': tags if TAGS_ENABLE else []} + if cached_update is not None and is_cache_valid: + container_info['update_available'] = cached_update + else: + container_info['update_available'] = update_checker.check_local_image_updates(client, container, server_name) + all_container_data.append(container_info) + except Exception as container_error: + logger.error(f"Error processing container {getattr(container, 'name', 'unknown')}: {container_error}") + all_container_data.append({'server': server_name, 'name': getattr(container, 'name', 'unknown'), 'status': 'error', 'image': 'error-loading', 'ports': []}) + except Exception as host_error: + logger.error(f"Error connecting to host {host['name']}: {host_error}") + for s in server_list_for_json: + if s["name"] == host["name"]: s["status"] = "inactive" + return {"servers": server_list_for_json, "containers": all_container_data, "traefik_enabled": TRAEFIK_ENABLE} \ No newline at end of file diff --git a/dockpeek/extensions.py b/dockpeek/extensions.py new file mode 100644 index 0000000..8860a48 --- /dev/null +++ b/dockpeek/extensions.py @@ -0,0 +1,7 @@ +from flask_cors import CORS +from flask_login import LoginManager + +# Inicjalizujemy rozszerzenia tutaj, aby uniknąć cyklicznych importów +cors = CORS() +login_manager = LoginManager() +login_manager.login_view = 'auth.login' # 'auth' to nazwa blueprint, 'login' to nazwa funkcji widoku \ No newline at end of file diff --git a/dockpeek/main.py b/dockpeek/main.py new file mode 100644 index 0000000..a93674f --- /dev/null +++ b/dockpeek/main.py @@ -0,0 +1,97 @@ +import json +from datetime import datetime +from concurrent.futures import ThreadPoolExecutor +from flask import ( + Blueprint, render_template, jsonify, request, current_app, make_response +) +from flask_login import login_required, current_user + +# Importujemy logikę Dockera z dedykowanego modułu +from .docker_utils import get_all_data, discover_docker_clients, update_checker + +main_bp = Blueprint('main', __name__) + +@main_bp.route("/") +@login_required +def index(): + version = current_app.config['APP_VERSION'] + return render_template("index.html", version=version) + +@main_bp.route("/data") +@login_required +def data(): + return jsonify(get_all_data()) + +@main_bp.route("/check-updates", methods=["POST"]) +@login_required +def check_updates(): + request_data = request.get_json() or {} + server_filter = request_data.get('server_filter', 'all') + + servers = discover_docker_clients() + active_servers = [s for s in servers if s['status'] == 'active'] + + if server_filter != 'all': + active_servers = [s for s in active_servers if s['name'] == server_filter] + + updates = {} + + def check_container_update(args): + server, container = args + try: + update_available = update_checker.check_image_updates_async( + server['client'], container, server['name'] + ) + return f"{server['name']}:{container.name}", update_available + except Exception: + return f"{server['name']}:{container.name}", False + + check_args = [] + for server in active_servers: + try: + for container in server['client'].containers.list(all=True): + check_args.append((server, container)) + except Exception as e: + current_app.logger.error(f"❌ Error accessing containers on {server['name']}: {e}") + + with ThreadPoolExecutor(max_workers=4) as executor: + results = executor.map(check_container_update, check_args) + for key, result in results: + updates[key] = result + + return jsonify({"updates": updates}) + + +@main_bp.route("/export/json") +@login_required +def export_json(): + server_filter = request.args.get('server', 'all') + data = get_all_data() + + filtered_containers = data.get("containers", []) + if server_filter != 'all': + filtered_containers = [c for c in filtered_containers if c.get("server") == server_filter] + + # ... (logika eksportu, która była w app.py, bez zmian) ... + export_data = { + "export_info": { + "timestamp": datetime.now().isoformat(), + "dockpeek_version": current_app.config['APP_VERSION'], + "server_filter": server_filter, + "total_containers": len(filtered_containers), + }, + "containers": [] + } + for c in filtered_containers: + export_container = {k: v for k, v in c.items() if k in ['name', 'server', 'stack', 'image', 'status', 'exit_code', 'custom_url']} + if c.get("ports"): export_container["ports"] = c["ports"] + if c.get("traefik_routes"): export_container["traefik_routes"] = [{"router": r["router"], "url": r["url"]} for r in c["traefik_routes"]] + export_data["containers"].append(export_container) + + formatted_json = json.dumps(export_data, indent=2, ensure_ascii=False) + filename = f'dockpeek-export-{server_filter}-{datetime.now().strftime("%Y%m%d-%H%M%S")}.json' + + response = make_response(formatted_json) + response.headers['Content-Disposition'] = f'attachment; filename={filename}' + response.headers['Content-Type'] = 'application/json' + return response \ No newline at end of file diff --git a/static/css/fonts.css b/dockpeek/static/css/fonts.css similarity index 100% rename from static/css/fonts.css rename to dockpeek/static/css/fonts.css diff --git a/static/css/styles.css b/dockpeek/static/css/styles.css similarity index 100% rename from static/css/styles.css rename to dockpeek/static/css/styles.css diff --git a/static/css/tailwindcss.css b/dockpeek/static/css/tailwindcss.css similarity index 100% rename from static/css/tailwindcss.css rename to dockpeek/static/css/tailwindcss.css diff --git a/static/fonts/inter/inter-v19-latin-500.woff2 b/dockpeek/static/fonts/inter/inter-v19-latin-500.woff2 similarity index 100% rename from static/fonts/inter/inter-v19-latin-500.woff2 rename to dockpeek/static/fonts/inter/inter-v19-latin-500.woff2 diff --git a/static/fonts/inter/inter-v19-latin-600.woff2 b/dockpeek/static/fonts/inter/inter-v19-latin-600.woff2 similarity index 100% rename from static/fonts/inter/inter-v19-latin-600.woff2 rename to dockpeek/static/fonts/inter/inter-v19-latin-600.woff2 diff --git a/static/fonts/inter/inter-v19-latin-700.woff2 b/dockpeek/static/fonts/inter/inter-v19-latin-700.woff2 similarity index 100% rename from static/fonts/inter/inter-v19-latin-700.woff2 rename to dockpeek/static/fonts/inter/inter-v19-latin-700.woff2 diff --git a/static/fonts/inter/inter-v19-latin-800.woff2 b/dockpeek/static/fonts/inter/inter-v19-latin-800.woff2 similarity index 100% rename from static/fonts/inter/inter-v19-latin-800.woff2 rename to dockpeek/static/fonts/inter/inter-v19-latin-800.woff2 diff --git a/static/fonts/inter/inter-v19-latin-regular.woff2 b/dockpeek/static/fonts/inter/inter-v19-latin-regular.woff2 similarity index 100% rename from static/fonts/inter/inter-v19-latin-regular.woff2 rename to dockpeek/static/fonts/inter/inter-v19-latin-regular.woff2 diff --git a/dockpeek/static/fonts/unused/fonts.css b/dockpeek/static/fonts/unused/fonts.css new file mode 100644 index 0000000..6ad2262 --- /dev/null +++ b/dockpeek/static/fonts/unused/fonts.css @@ -0,0 +1,144 @@ +/* inter-100 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + src: url('/static/fonts/inter/inter-v19-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-100italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 100; + src: url('/static/fonts/inter/inter-v19-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-200 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + src: url('/static/fonts/inter/inter-v19-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-200italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 200; + src: url('/static/fonts/inter/inter-v19-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-300 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + src: url('/static/fonts/inter/inter-v19-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-300italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 300; + src: url('/static/fonts/inter/inter-v19-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url('/static/fonts/inter/inter-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + src: url('/static/fonts/inter/inter-v19-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-500 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('/static/fonts/inter/inter-v19-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-500italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + src: url('/static/fonts/inter/inter-v19-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + src: url('/static/fonts/inter/inter-v19-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-600italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + src: url('/static/fonts/inter/inter-v19-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-700 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url('/static/fonts/inter/inter-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-700italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + src: url('/static/fonts/inter/inter-v19-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-800 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + src: url('/static/fonts/inter/inter-v19-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-800italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 800; + src: url('/static/fonts/inter/inter-v19-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-900 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + src: url('/static/fonts/inter/inter-v19-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-900italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 900; + src: url('/static/fonts/inter/inter-v19-latin-900italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} \ No newline at end of file diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-100.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-100.woff2 new file mode 100644 index 0000000..fbcfb9e Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-100.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-100italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-100italic.woff2 new file mode 100644 index 0000000..8427f11 Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-100italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-200.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-200.woff2 new file mode 100644 index 0000000..37267df Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-200.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-200italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-200italic.woff2 new file mode 100644 index 0000000..cb15f8d Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-200italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-300.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-300.woff2 new file mode 100644 index 0000000..ece952c Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-300.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-300italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-300italic.woff2 new file mode 100644 index 0000000..dd92d3b Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-300italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-500italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-500italic.woff2 new file mode 100644 index 0000000..f4f25da Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-500italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-600italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-600italic.woff2 new file mode 100644 index 0000000..e882c78 Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-600italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-700italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-700italic.woff2 new file mode 100644 index 0000000..b6a7cad Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-700italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-800italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-800italic.woff2 new file mode 100644 index 0000000..e98fa7e Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-800italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-900.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-900.woff2 new file mode 100644 index 0000000..4db8333 Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-900.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-900italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-900italic.woff2 new file mode 100644 index 0000000..291eafc Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-900italic.woff2 differ diff --git a/dockpeek/static/fonts/unused/inter-v19-latin-italic.woff2 b/dockpeek/static/fonts/unused/inter-v19-latin-italic.woff2 new file mode 100644 index 0000000..9e98286 Binary files /dev/null and b/dockpeek/static/fonts/unused/inter-v19-latin-italic.woff2 differ diff --git a/static/js/app.js b/dockpeek/static/js/app.js similarity index 100% rename from static/js/app.js rename to dockpeek/static/js/app.js diff --git a/static/logo.svg b/dockpeek/static/logo.svg similarity index 100% rename from static/logo.svg rename to dockpeek/static/logo.svg diff --git a/static/logo_2.svg b/dockpeek/static/logo_2.svg similarity index 100% rename from static/logo_2.svg rename to dockpeek/static/logo_2.svg diff --git a/templates/index.html b/dockpeek/templates/index.html similarity index 100% rename from templates/index.html rename to dockpeek/templates/index.html diff --git a/templates/login.html b/dockpeek/templates/login.html similarity index 100% rename from templates/login.html rename to dockpeek/templates/login.html diff --git a/requirements.txt b/requirements.txt index e237c52..479eb02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -flask -flask-cors -flask-login -docker +Flask +Flask-Cors +Flask-Login werkzeug -packaging \ No newline at end of file +docker +packaging +gunicorn # (opcjonalnie, do wdrożeń produkcyjnych) \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..3b8f94f --- /dev/null +++ b/run.py @@ -0,0 +1,8 @@ +import os +from dockpeek import create_app + +app = create_app() + +if __name__ == "__main__": + debug = os.environ.get("FLASK_DEBUG", "false").lower() == "true" + app.run(host="0.0.0.0", port=8000, debug=debug) \ No newline at end of file diff --git a/static_old/css/fonts.css b/static_old/css/fonts.css new file mode 100644 index 0000000..380bbc3 --- /dev/null +++ b/static_old/css/fonts.css @@ -0,0 +1,41 @@ + +/* inter-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url('/static/fonts/inter/inter-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-500 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('/static/fonts/inter/inter-v19-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + src: url('/static/fonts/inter/inter-v19-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-700 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url('/static/fonts/inter/inter-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-800 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + src: url('/static/fonts/inter/inter-v19-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} \ No newline at end of file diff --git a/static_old/css/styles.css b/static_old/css/styles.css new file mode 100644 index 0000000..ebf57a2 --- /dev/null +++ b/static_old/css/styles.css @@ -0,0 +1,1477 @@ +@import "./fonts.css"; +@import "tailwindcss"; + +body { + font-family: "Inter", sans-serif; + background-color: #f6f8fa; + color: #24292e; +} + +.login-box { + background-color: #161b22; +} + +.login-box input { + background-color: #0d1117; + border-color: #30363d; + color: #c9d1d9; +} + +.login-box input:focus { + outline: none; + border-color: #58a6ff; + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.3); +} + +.login-box label { + color: #8b949e; +} + +button svg { + width: 18px; + height: 18px; +} + +.login-box button { + background-color: #238636; +} + +.login-box button:hover { + background-color: #2ea043; +} + +.container { + background-color: #ffffff; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +.logo-title { + margin-right: 1em; + color: #e1e4e8; + cursor: pointer; +} + +body.dark-mode .logo-title { + color: #2a2f37; +} + +.github { + color: #acb1b9; +} + +body.dark-mode .github { + color: #6a737d; +} + +.footer { + color: #b7b9bd; + background-color: transparent !important; + border-radius: 0; + box-shadow: none !important; +} + +.footer-center { + text-align: center; +} + +body.dark-mode .footer { + color: #424b5d; +} + +.status-none { + color: #b8bcc3; +} + +body.dark-mode .status-none { + color: #474c55; +} + + +[data-content="ports"] svg { + color: #b8bcc3; +} + +body.dark-mode [data-content="ports"] svg { + color: #474c55; +} + +.data-content-name { + font-weight: 500; + color: #3e4043; +} + +.data-content-server { + color: #6a737d; +} + +body.dark-mode .data-content-server { + color: #8b949e; +} + +body.dark-mode .data-content-name { + color: #c9d1d9; +} + +.text-gray-800 { + color: #949da7; +} + +.text-gray-600 { + color: #586069; +} + +.text-gray-700 { + color: #24292e; +} + +.text-gray-900 { + color: #30363d; +} + +.bg-gray-100 { + background-color: #f3f4f6; +} + +.border-gray-200 { + border-color: #e1e4e8; +} + +.hover\:bg-gray-50:hover { + background-color: #fafafa !important; +} + +.text-muted, +.text-secondary { + color: #6a737d; +} + +[data-content="ports"] .badge { + background-color: #e1e4e8; + color: #2188ff; + border-radius: 0.375rem; + padding: 0.08rem 0.35em; + font-weight: 400; + display: inline-block; +} + +body.dark-mode [data-content="ports"] .badge { + background-color: #21262d; + color: #2188ff; +} + +[data-content="ports"] .custom-port .badge { + color: #959595 !important; +} + +body.dark-mode [data-content="ports"] .custom-port .badge { + color: #6d7789 !important; +} + +.sortable-header { + cursor: pointer; + position: relative; + padding-right: 20px; +} + +.sortable-header::after { + content: ""; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #24292e; +} + +.sortable-header.desc::after { + border-top: 5px solid #24292e; + border-bottom: none; +} + +input[type="text"] { + background-color: #f6f8fa; + border: 1px solid #ebebeb; + border-radius: 0.5rem; + color: #54595f; + padding: 0.75rem; + width: 100%; +} + +#search-input { + outline: none; + box-shadow: 0 0 0 2px #ebebeb !important; + border-color: transparent; +} + +#search-input:focus { + box-shadow: 0 0 0 2px #e1e4e8 !important; +} + +body.dark-mode #search-input { + box-shadow: 0 0 0 2px #22262d !important; +} + +body.dark-mode #search-input:focus { + box-shadow: 0 0 0 2px #31363d !important; +} + +#search-input::placeholder { + color: #bbbfc5; +} + +#theme-switcher, +#refresh-button { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + background-color: #e1e4e8; + color: #626c78; + padding: 0.5rem 1rem; + border-radius: 0.5rem; +} + +#theme-switcher:hover, +#refresh-button:hover { + background-color: #d1d5da; +} + +#theme-switcher:focus, +#refresh-button:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(108, 117, 125, 0.75); +} + +.source-link[data-tooltip-top-right]::after { + max-width: 600px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} + +.source-link:not(.hidden) { + color: #99a1af !important; + align-items: center; + text-decoration: none; + display: inline-block; + padding-left: 4px; +} +svg { + pointer-events: none; +} +.source-link svg { + display: block; +} + + +.source-link:hover { + color: #2b7fff !important; +} + +body.dark-mode .source-link { + color: #474c55 !important; +} + +body.dark-mode .source-link:hover { + color: #2b7fff !important; +} + +.server-column [data-content="server-name"] { + color: #808993; +} + + +body.dark-mode .server-column [data-content="server-name"] { + color: #5a626b; +} + +.container-name-wrapper {} + +.container-name-cell [data-tooltip-right]::after { + max-width: 600px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} + +/* Custom link color for container names */ +.container-name-cell { + color: #4e5051 !important; +} + +body.dark-mode .container-name-cell { + color: #e1e1e1 !important; +} + +.container-name-cell a { + color: #3e71e9 !important; +} + +.container-name-cell a:hover { + color: #2b7fff !important; +} + +/* Dark mode version */ +body.dark-mode .container-name-cell a { + color: #85a3d1 !important; +} + +body.dark-mode .container-name-cell a:hover { + color: #2b7fff !important; +} + +.table-cell-stack a { + color: #838c95 !important; +} + +.table-cell-stack a:hover { + color: #2b7fff !important; +} + +/* Dark mode version */ +body.dark-mode .table-cell-stack a { + color: #737b85 !important; +} + +body.dark-mode .table-cell-stack a:hover { + color: #2b7fff !important; +} + +/* +#refresh-button.loading svg { + animation: spin 0.8s linear infinite; +} +*/ +.text-red-500 { + color: #cb2431; +} + +.table-cell-status { + text-align: center; +} + +.table-cell-status, +.server-column { + cursor: default; +} + +/* Green - OK/Running states */ +.status-running { + color: #28a745; + font-weight: 400; +} + +.status-healthy { + color: #28a745; + font-weight: 400; +} + +/* Red - Error/Failed states */ +.status-exited { + color: #dc3545; + font-weight: 400; +} + +.status-unhealthy { + color: #dc3545; + font-weight: 400; +} + +.status-dead { + color: #dc3545; + font-weight: 400; +} + +.status-unknown { + color: #dc3545; + font-weight: 400; +} + +/* Orange - Transitional/Warning states */ +.status-starting { + color: #99a1af; + font-weight: 400; +} + +.status-restarting { + color: #99a1af; + font-weight: 400; +} + +.status-removing { + color: #99a1af; + font-weight: 400; +} + +/* Gray/White - Temporary/Inactive states */ +.status-paused { + color: #fd7e14; + font-weight: 400; +} + +.status-created { + color: #99a1af; + font-weight: 400; +} + +.status-none { + color: #99a1af; + font-weight: 400; +} + +code { + background-color: #f3f4f6; + color: #24292e; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; +} + +.filter-button { + padding: 0.4rem 1rem; + border-radius: 0.5rem; + background-color: #e1e4e8; + color: #3e4043; + font-weight: 500; + cursor: pointer; + box-shadow: 0.2s ease-in-out; +} + +.filter-button:hover { + background-color: #e9eaeb; +} + +.filter-button.active { + background-color: #2188ff; + color: white; +} + +.filter-button.inactive { + color: #d2d3d3; + background-color: #f3f3f3; +} + +body.dark-mode .filter-button { + background-color: #232831; + color: #b3b9bf; +} + +body.dark-mode .filter-button:hover { + background-color: #202329; +} + +body.dark-mode .filter-button.active { + background-color: #2188ff; + color: white; +} + +body.dark-mode .filter-button.inactive { + color: #30353d; + background-color: #15181c; +} + +.table-single-server .server-column { + display: none; +} + +/* Dark Mode Styles */ +body.dark-mode { + background-color: #0d1117; + color: #c9d1d9; +} + +body.dark-mode .container { + background-color: #161b22; +} + +body.dark-mode .text-gray-800 { + color: #24292e; +} + +body.dark-mode .text-gray-600 { + color: #8b949e; +} + +body.dark-mode .text-gray-700 { + color: #c9d1d9; +} + +body.dark-mode .text-gray-900 { + color: #c9d1d9; +} + +body.dark-mode .bg-gray-100 { + background-color: #21262d; +} + +body.dark-mode .border-gray-200 { + border-color: #232831; +} + +body.dark-mode table { + background-color: #161b22; + border-color: #232831; +} + +.main-table-header { + background-color: #f7f7f7; + color: #c7c7c7; +} + +body.dark-mode .main-table-header { + background-color: #21262d; + color: #c7c7c7; +} + +body.dark-mode .hover\:bg-gray-50:hover { + background-color: #21262d !important; +} + +body.dark-mode .text-muted, +body.dark-mode .text-secondary { + color: #8b949e; +} + +body.dark-mode .sortable-header::after { + border-bottom-color: #c9d1d9; + border-top-color: #c9d1d9; +} + +body.dark-mode input[type="text"] { + background-color: #0d1117; + border-color: #30363d; + color: #aab0b5; +} + +body.dark-mode #search-input::placeholder { + color: #41464b; +} + +body.dark-mode #theme-switcher, +body.dark-mode #refresh-button { + background-color: #21262d; + color: #97a1ab; +} + +body.dark-mode #theme-switcher:hover, +body.dark-mode #refresh-button:hover { + background-color: #30363d; +} + +body.dark-mode .text-red-500 { + color: #f85149; +} + +body.dark-mode code { + background-color: #21262d; + color: #c9d1d9; +} + +/* Confirmation Modal Styles */ +.modal-overlay:not(.hidden) { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modal-content:not(.hidden) { + background-color: #ffffff; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 700px; + max-height: 80vh; + text-align: center; + display: flex; + flex-direction: column; +} + +body.dark-mode .modal-content { + background-color: #161b22; + border: 1px solid #30363d; +} + +body.dark-mode #modal-cancel-button { + background-color: #21262d; + color: #c9d1d9; +} + +body.dark-mode #modal-cancel-button:hover { + background-color: #30363d; +} + +/* Updates Modal Styles */ + +#updates-modal .modal-content:not(.hidden) { + max-width: 900px !important; +} + +#updates-modal-message { + margin-bottom: 1.5rem; + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +#updates-list { + background-color: #f8f9fa; + padding: 1rem; + border-radius: 0.375rem; + border: 1px solid #e1e4e8; + max-height: 500px; + overflow-y: auto; + flex: 1; + min-height: 100px; +} + +#updates-list li { + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + padding: 0.5rem 0; + border-bottom: 1px solid #e1e4e8; +} + +#updates-list li:last-child { + border-bottom: none; +} + +#updates-list .stack-name { + color: #a6b4c3; + font-weight: normal; + font-size: 0.857rem; + flex-shrink: 0; +} + +#updates-list .server-name { + color: #a1aab4; + font-weight: normal; + font-size: 0.875rem; + flex-shrink: 0; +} + +#updates-list .image-name { + color: #a6b4c3; + font-style: italic; + font-size: 0.8rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + text-align: left; + min-width: 0; +} + +body.dark-mode #updates-list { + background-color: #21262d; + border-color: #30363d; +} + +body.dark-mode #updates-list li { + border-bottom-color: #30363d; +} + +body.dark-mode #updates-list .server-name { + color: #6a737d; +} + +body.dark-mode #updates-list .image-name { + color: #6a737d; +} + +#updates-list .no-updates-message { + list-style: none; + color: #28a745; + font-weight: 600; + font-size: 1rem; + white-space: normal; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + padding: 2rem 1rem; +} + +body.dark-mode #updates-list .no-updates-message { + color: #56d364; +} + +/* Custom scrollbar */ +#updates-list::-webkit-scrollbar { + width: 8px; +} + +#updates-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +#updates-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +#updates-list::-webkit-scrollbar-thumb:hover { + background: #a1a1a1; +} + +body.dark-mode #updates-list::-webkit-scrollbar-track { + background: #30363d; +} + +body.dark-mode #updates-list::-webkit-scrollbar-thumb { + background: #6a737d; +} + +body.dark-mode #updates-list::-webkit-scrollbar-thumb:hover { + background: #8b949e; +} + +/* Loading Spinner */ +.loader { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 2rem auto; +} + +body.dark-mode .loader { + border-color: #21262d; + border-top-color: #2188ff; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes spin-reverse { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(-360deg); + } +} + +/* Update indicator styles */ +.update-indicator:not(.hidden) { + display: inline-flex; + align-items: center; +} + +.update-indicator svg { + width: 20px; + height: 20px; + color: #d67a10; +} + +body.dark-mode .update-indicator svg { + color: #b9711f; +} + +.image-wrapper { + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-width: 200px; +} + +.image-wrapper-inline { + display: inline; +} + + +.table-cell-image code { + display: inline; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + background-color: #ededed; + color: #3e4043; + border-radius: 0.25rem; + padding: 0.2rem 0.4rem; + white-space: break-spaces; + word-break: break-all; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} + +body.dark-mode .table-cell-image code { + background-color: #22262d; + color: #d5d5d5; +} + +#check-updates-button { + background-color: #e1e4e8; + color: #626c78; + padding: 0.5rem 1rem; + border-radius: 0.5rem; +} + +#check-updates-button:hover { + background-color: #d1d5da; +} + +/* Ukryj SVG podczas ładowania */ + +#check-updates-button svg { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-right: 0.5rem; +} + +#check-updates-button.loading svg { + display: none; +} + +/* Pokaż spinner podczas ładowania */ +#check-updates-button.loading::before { + content: ""; + display: inline-block; + border: 2px solid #f3f3f3; + border-top: 2px solid #d67a10; + border-radius: 50%; + width: 18px; + height: 18px; + animation: spin 1s linear infinite; + vertical-align: middle; + margin-right: 0.5rem; +} + +/* Animacja spinnera */ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +/* Dark mode */ +body.dark-mode #check-updates-button { + background-color: #21262d; + color: #97a1ab; +} + +body.dark-mode #check-updates-button:hover { + background-color: #30363d; +} + +/* Dark mode spinner */ +body.dark-mode #check-updates-button.loading::before { + border: 2px solid #30363d; + border-top: 2px solid #b9711f; +} + +/* Toggle Switch Styles */ +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #e1e4e8; + border-radius: 24px; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + transition: transform 0.1s ease-in-out; +} + +input:checked+.slider { + background-color: #3f88fe; +} + +input:checked+.slider:before { + transform: translateX(20px); +} + +body.dark-mode .slider { + background-color: #33393f; +} + +body.dark-mode .slider:before { + background-color: #181c25; +} + +body.dark-mode input:checked+.slider { + background-color: #567bd4; +} + +#filter-updates-checkbox:checked+.slider { + background-color: #d67a10; +} + +body.dark-mode #filter-updates-checkbox:checked+.slider { + background-color: #d67a10; +} + +#filter-running-checkbox:checked+.slider { + background-color: #3ec93c; +} + +body.dark-mode #filter-running-checkbox:checked+.slider { + background-color: #3b8531; +} + +label[for="filter-running-checkbox"] { + color: #babdc1; +} + +body.dark-mode label[for="filter-running-checkbox"] { + color: #4d525b; +} + +#modal-message { + white-space: pre-line; + line-height: 1.6; +} + +button:focus { + outline: none !important; + box-shadow: none !important; +} + +/* Kolory dla linków Traefik */ +.traefik-route a { + color: #3e71e9 !important; +} + +.traefik-route a:hover { + color: #2b7fff !important; +} + +/* Dark mode version */ +body.dark-mode .traefik-route a { + color: #2572cb !important; +} + +body.dark-mode .traefik-route a:hover { + color: #2b7fff !important; +} + +/* Column Menu Styles */ +.column-menu-container { + margin-left: 16px; + position: relative; + display: inline-block; +} + +.column-menu-reset { + text-align: right; + /* ustawia zawartość po prawej */ +} + +.reset-columns-btn { + display: inline-block; + color: #767983; + font-weight: normal; + padding-top: 0.75em; + padding-right: 0.3em; +} +/* Klikalny kontener (div) */ +.column-menu-container { + height: 32px; + width: 32px; + cursor: pointer; + border-radius: 0.375rem; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* Ikona w środku */ +.column-menu-svg { + height: 26px; + width: 26px; + color: #c0c4c9; + pointer-events: none; /* <-- ważne, żeby klik szedł w kontener, a nie w SVG */ +} + +/* Hover dla całego przycisku */ +.column-menu-container:hover .column-menu-svg { + color: #a2a6aa; +} + +/* Dark mode */ +body.dark-mode .column-menu-svg { + color: #767e87; +} + +body.dark-mode .column-menu-container:hover .column-menu-svg { + color: #40464d; +} + + +.column-menu { + position: absolute; + top: calc(100% + 0px); + right: 0; + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 0.5rem; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1); + padding: 0.75rem; + min-width: 180px; + z-index: 1000; +} + +.column-menu-item { + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + font-size: 0.875rem; +} + +.column-menu-item:not(:last-child) { + border-bottom: 1px solid #f1f3f4; +} + +.column-menu-item span { + color: #24292e; + font-weight: 400; +} + +/* Dark mode styles */ +body.dark-mode .column-menu-button { + background-color: #21262d; + color: #97a1ab; +} + +body.dark-mode .column-menu-button:hover { + background-color: #30363d; +} + +body.dark-mode .column-menu { + background-color: #1c2129; + border-color: #282d33; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.3); +} + +body.dark-mode .column-menu-item { + border-bottom-color: #282d33; +} + +body.dark-mode .column-menu-item span { + color: #c9d1d9; +} + +/* Column visibility classes */ +.column-hidden { + display: none !important; +} + +/* Responsive Styles */ +@media (max-width: 576px) { + body { + padding: 0; + } + + #search-input::placeholder { + opacity: 0; + } + + .container { + padding: 0.5rem; + } + + .flex.justify-between.items-center.mb-6 { + flex-direction: column; + align-items: flex-start; + margin-bottom: 1rem; + } + + .controls-container { + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 1rem; + } + + .controls-container>* { + margin-bottom: 0.5rem; + width: 100%; + } + + .py-3.px-4 { + padding: 0.5rem 0.75rem; + } + + .min-w-full { + min-width: unset; + } + + .overflow-x-auto { + /* overflow-x: hidden;*/ + } + + .update-indicator { + display: none; + } + + .min-w-full { + min-width: 100%; + } + +} + +/* Responsive Styles */ +@media (max-width: 768px) { + + .container { + padding: 0.5rem; + } + + .column-menu-container { + margin-left: 8px; + } + + .right-18 { + right: calc(var(--spacing) * 14); + } + + [data-tooltip-left]::after, + [data-tooltip-left]::before, + [data-tooltip-right]::after, + [data-tooltip-right]::before, + [data-tooltip]::after, + [data-tooltip]::before { + display: none !important; + content: none !important; + } + + .flex.justify-between.items-center.mb-6 { + flex-direction: column; + align-items: flex-start; + margin-bottom: 1rem; + } + + .status-filter { + display: flex; + justify-content: right; + align-items: center; + height: 100%; + } + + .column-menu-container { + display: flex; + justify-content: right; + align-items: center; + height: 100%; + } + + .controls-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 1rem; + } + + .controls-container>* { + margin-bottom: 0.5rem; + width: 100%; + } + + .py-3.px-4 { + padding: 0.5rem 0.75rem; + } + + .min-w-full { + min-width: unset; + } + + .min-w-full { + min-width: 100%; + } +} + +.traefik-route a { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.traefik-route a .traefik-text { + display: inline; +} + + +/* domyślnie (dla dużych ekranów) */ +label[for="filter-running-checkbox"] { + display: inline; +} + +/* ukryj dla ekranów <= 1023px */ +@media (max-width: 1023px) { + label[for="filter-running-checkbox"] { + display: none; + } +} + +/* przywróć widoczność dla ekranów <= 768px (nadpisze regułę powyżej) */ +@media (max-width: 768px) { + label[for="filter-running-checkbox"] { + display: inline; + } +} + +/* jawne ustawienie dla >= 1024px (opcjonalne, domyślne już to daje) */ +@media (min-width: 1024px) { + label[for="filter-running-checkbox"] { + display: inline; + } +} + +.tag-badge { + max-width: 120px; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + background-color: #ededed; + color: #727373; + font-size: 0.75rem; + font-weight: 400; + padding: 2px 7px; + border-radius: 9999px; + cursor: pointer; +} + +.tag-badge:hover { + background-color: #e1e4e8; +} + +.dark-mode .tag-badge { + background-color: #22262d; + color: #90939b; +} + +.dark-mode .tag-badge:hover { + background-color: #2d3137; +} + +.tags-container { + display: inline-flex; + flex-wrap: wrap; + gap: 4px; + max-width: 250px; +} + +.column-menu { + min-width: 200px; +} + +.column-menu-header { + padding: 8px 12px; + border-bottom: 1px solid #e5e7eb; + margin-bottom: 8px; +} + +.column-list { + max-height: 400px; + overflow-y: auto; +} + +.column-menu-item.draggable { + display: flex; + align-items: center; + cursor: move; + padding: 8px 4px; + border: 1px solid transparent; + border-radius: 4px; + margin: 2px 8px; +} + +.column-menu-item.dragging { + opacity: 0.5; + background-color: #f3f4f6; + border-color: #d1d5db; +} + + +.dark-mode .column-menu-item.dragging { + opacity: 0.5; + background-color: #454850; + border-color: #373b44; +} + + +.column-menu-item.drag-over { + border-top: 2px solid #3b82f6; +} + +.drag-handle { + color: #9ca3af; + font-weight: bold; + margin-right: 8px; + user-select: none; + line-height: 1; +} + +.drag-handle:hover { + color: #6b7280; +} + +.column-menu-item label { + flex: 1; + cursor: pointer; + display: flex; + justify-content: space-between; +} + +/* ======================================== */ +/* Custom Tooltip Styles (JS-Driven) */ +/* ======================================== */ + +/* Główny kontener tooltipa, tworzony przez JS. */ +.custom-tooltip { + position: absolute; + /* Pozycję (top, left) ustawi JavaScript */ + z-index: 1000; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; +} + +/* Klasa dodawana przez JS, aby pokazać tooltip */ +.custom-tooltip.is-visible { + opacity: 1; + visibility: visible; +} + +/* Dymek tooltipa */ +.custom-tooltip-box { + background-color: #ffffff; + color: #4f5359; + font-size: 0.8rem; + font-weight: 400; + padding: 0.4rem 0.8rem; + border-radius: 0.375rem; + white-space: nowrap; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.2); + max-width: 600px; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Strzałka/trójkąt */ +.custom-tooltip-arrow { + content: ""; + position: absolute; + width: 0; + height: 0; + border-style: solid; +} + +/* --- Modyfikatory Pozycji Strzałki --- */ + +/* Pozycja: Góra (domyślna) */ +.custom-tooltip-arrow.arrow-top { + border-width: 6px 6px 0 6px; + border-color: #ffffff transparent transparent transparent; + left: 50%; + top: 100%; + transform: translateX(-50%); +} + +/* Pozycja: Lewo */ +.custom-tooltip-arrow.arrow-left { + border-width: 10px 0 10px 10px; + border-color: transparent transparent transparent #ffffff; + top: 50%; + left: 100%; + transform: translateY(-50%); +} + +/* Pozycja: Prawo */ +.custom-tooltip-arrow.arrow-right { + border-width: 10px 10px 10px 0; + border-color: transparent #ffffff transparent transparent; + top: 50%; + right: 100%; + transform: translateY(-50%); +} + + +/* === Style Tooltipa dla Trybu Ciemnego === */ +body.dark-mode .custom-tooltip-box { + background-color: #31363d; + color: #f6f8fa; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.4); +} + +body.dark-mode .custom-tooltip-arrow.arrow-top { + border-top-color: #31363d; +} + +body.dark-mode .custom-tooltip-arrow.arrow-left { + border-left-color: #31363d; +} + +body.dark-mode .custom-tooltip-arrow.arrow-right { + border-right-color: #31363d; +} \ No newline at end of file diff --git a/static_old/css/tailwindcss.css b/static_old/css/tailwindcss.css new file mode 100644 index 0000000..8c621c7 --- /dev/null +++ b/static_old/css/tailwindcss.css @@ -0,0 +1,1986 @@ +/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ +@layer properties; +@font-face { + font-display: swap; + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url('/static/fonts/inter/inter-v19-latin-regular.woff2') format('woff2'); +} +@font-face { + font-display: swap; + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('/static/fonts/inter/inter-v19-latin-500.woff2') format('woff2'); +} +@font-face { + font-display: swap; + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + src: url('/static/fonts/inter/inter-v19-latin-600.woff2') format('woff2'); +} +@font-face { + font-display: swap; + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url('/static/fonts/inter/inter-v19-latin-700.woff2') format('woff2'); +} +@font-face { + font-display: swap; + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + src: url('/static/fonts/inter/inter-v19-latin-800.woff2') format('woff2'); +} +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-900: oklch(21% 0.034 264.665); + --color-white: #fff; + --spacing: 0.25rem; + --container-md: 28rem; + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --radius-lg: 0.5rem; + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .visible { + visibility: visible; + } + .absolute { + position: absolute; + } + .relative { + position: relative; + } + .right-18 { + right: calc(var(--spacing) * 18); + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .mx-auto { + margin-inline: auto; + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mr-3 { + margin-right: calc(var(--spacing) * 3); + } + .mb-1 { + margin-bottom: calc(var(--spacing) * 1); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .block { + display: block; + } + .flex { + display: flex; + } + .hidden { + display: none; + } + .inline-block { + display: inline-block; + } + .table { + display: table; + } + .h-20 { + height: calc(var(--spacing) * 20); + } + .min-h-screen { + min-height: 100vh; + } + .w-20 { + width: calc(var(--spacing) * 20); + } + .w-full { + width: 100%; + } + .max-w-md { + max-width: var(--container-md); + } + .min-w-full { + min-width: 100%; + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .cursor-pointer { + cursor: pointer; + } + .list-none { + list-style-type: none; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .space-x-1 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-2 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-3 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-4 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-9 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 9) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 9) * calc(1 - var(--tw-space-x-reverse))); + } + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-auto { + overflow-x: auto; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-gray-200 { + border-color: var(--color-gray-200); + } + .border-gray-300 { + border-color: var(--color-gray-300); + } + .bg-blue-500 { + background-color: var(--color-blue-500); + } + .bg-gray-100 { + background-color: var(--color-gray-100); + } + .bg-gray-200 { + background-color: var(--color-gray-200); + } + .bg-red-500 { + background-color: var(--color-red-500); + } + .bg-white { + background-color: var(--color-white); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-3 { + padding: calc(var(--spacing) * 3); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } + .pr-12 { + padding-right: calc(var(--spacing) * 12); + } + .text-center { + text-align: center; + } + .text-left { + text-align: left; + } + .align-middle { + vertical-align: middle; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .whitespace-pre-line { + white-space: pre-line; + } + .text-blue-600 { + color: var(--color-blue-600); + } + .text-gray-400 { + color: var(--color-gray-400); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-gray-600 { + color: var(--color-gray-600); + } + .text-gray-700 { + color: var(--color-gray-700); + } + .text-gray-900 { + color: var(--color-gray-900); + } + .text-red-500 { + color: var(--color-red-500); + } + .text-white { + color: var(--color-white); + } + .shadow-xl { + --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-150 { + --tw-duration: 150ms; + transition-duration: 150ms; + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .select-none { + -webkit-user-select: none; + user-select: none; + } + .hover\:bg-blue-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-600); + } + } + } + .hover\:bg-gray-50 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-50); + } + } + } + .hover\:bg-gray-300 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-300); + } + } + } + .hover\:bg-red-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-600); + } + } + } + .hover\:text-blue-500 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-500); + } + } + } + .hover\:text-blue-600 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-600); + } + } + } + .hover\:text-blue-800 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-800); + } + } + } + .hover\:text-gray-600 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-600); + } + } + } + .focus\:border-transparent { + &:focus { + border-color: transparent; + } + } + .focus\:ring-2 { + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus\:ring-blue-500 { + &:focus { + --tw-ring-color: var(--color-blue-500); + } + } + .focus\:ring-gray-400 { + &:focus { + --tw-ring-color: var(--color-gray-400); + } + } + .focus\:ring-red-400 { + &:focus { + --tw-ring-color: var(--color-red-400); + } + } + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + .md\:inline { + @media (width >= 48rem) { + display: inline; + } + } + .dark\:text-gray-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-gray-400); + } + } +} +body { + font-family: "Inter", sans-serif; + background-color: #f6f8fa; + color: #24292e; +} +.login-box { + background-color: #161b22; +} +.login-box input { + background-color: #0d1117; + border-color: #30363d; + color: #c9d1d9; +} +.login-box input:focus { + outline: none; + border-color: #58a6ff; + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.3); +} +.login-box label { + color: #8b949e; +} +button svg { + width: 18px; + height: 18px; +} +.login-box button { + background-color: #238636; +} +.login-box button:hover { + background-color: #2ea043; +} +.container { + background-color: #ffffff; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} +.logo-title { + margin-right: 1em; + color: #e1e4e8; + cursor: pointer; +} +body.dark-mode .logo-title { + color: #2a2f37; +} +.github { + color: #acb1b9; +} +body.dark-mode .github { + color: #6a737d; +} +.footer { + color: #b7b9bd; + background-color: transparent !important; + border-radius: 0; + box-shadow: none !important; +} +.footer-center { + text-align: center; +} +body.dark-mode .footer { + color: #424b5d; +} +.status-none { + color: #b8bcc3; +} +body.dark-mode .status-none { + color: #474c55; +} +[data-content="ports"] svg { + color: #b8bcc3; +} +body.dark-mode [data-content="ports"] svg { + color: #474c55; +} +.data-content-name { + font-weight: 500; + color: #3e4043; +} +.data-content-server { + color: #6a737d; +} +body.dark-mode .data-content-server { + color: #8b949e; +} +body.dark-mode .data-content-name { + color: #c9d1d9; +} +.text-gray-800 { + color: #949da7; +} +.text-gray-600 { + color: #586069; +} +.text-gray-700 { + color: #24292e; +} +.text-gray-900 { + color: #30363d; +} +.bg-gray-100 { + background-color: #f3f4f6; +} +.border-gray-200 { + border-color: #e1e4e8; +} +.hover\:bg-gray-50:hover { + background-color: #fafafa !important; +} +.text-muted, .text-secondary { + color: #6a737d; +} +[data-content="ports"] .badge { + background-color: #e1e4e8; + color: #2188ff; + border-radius: 0.375rem; + padding: 0.08rem 0.35em; + font-weight: 400; + display: inline-block; +} +body.dark-mode [data-content="ports"] .badge { + background-color: #21262d; + color: #2188ff; +} +[data-content="ports"] .custom-port .badge { + color: #959595 !important; +} +body.dark-mode [data-content="ports"] .custom-port .badge { + color: #6d7789 !important; +} +.sortable-header { + cursor: pointer; + position: relative; + padding-right: 20px; +} +.sortable-header::after { + content: ""; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #24292e; +} +.sortable-header.desc::after { + border-top: 5px solid #24292e; + border-bottom: none; +} +input[type="text"] { + background-color: #f6f8fa; + border: 1px solid #ebebeb; + border-radius: 0.5rem; + color: #54595f; + padding: 0.75rem; + width: 100%; +} +#search-input { + outline: none; + box-shadow: 0 0 0 2px #ebebeb !important; + border-color: transparent; +} +#search-input:focus { + box-shadow: 0 0 0 2px #e1e4e8 !important; +} +body.dark-mode #search-input { + box-shadow: 0 0 0 2px #22262d !important; +} +body.dark-mode #search-input:focus { + box-shadow: 0 0 0 2px #31363d !important; +} +#search-input::placeholder { + color: #bbbfc5; +} +#theme-switcher, #refresh-button { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + background-color: #e1e4e8; + color: #626c78; + padding: 0.5rem 1rem; + border-radius: 0.5rem; +} +#theme-switcher:hover, #refresh-button:hover { + background-color: #d1d5da; +} +#theme-switcher:focus, #refresh-button:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(108, 117, 125, 0.75); +} +.source-link[data-tooltip-top-right]::after { + max-width: 600px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} +.source-link:not(.hidden) { + color: #99a1af !important; + align-items: center; + text-decoration: none; + display: inline-block; + padding-left: 4px; +} +svg { + pointer-events: none; +} +.source-link svg { + display: block; +} +.source-link:hover { + color: #2b7fff !important; +} +body.dark-mode .source-link { + color: #474c55 !important; +} +body.dark-mode .source-link:hover { + color: #2b7fff !important; +} +.server-column [data-content="server-name"] { + color: #808993; +} +body.dark-mode .server-column [data-content="server-name"] { + color: #5a626b; +} +.container-name-cell [data-tooltip-right]::after { + max-width: 600px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} +.container-name-cell { + color: #4e5051 !important; +} +body.dark-mode .container-name-cell { + color: #e1e1e1 !important; +} +.container-name-cell a { + color: #3e71e9 !important; +} +.container-name-cell a:hover { + color: #2b7fff !important; +} +body.dark-mode .container-name-cell a { + color: #85a3d1 !important; +} +body.dark-mode .container-name-cell a:hover { + color: #2b7fff !important; +} +.table-cell-stack a { + color: #838c95 !important; +} +.table-cell-stack a:hover { + color: #2b7fff !important; +} +body.dark-mode .table-cell-stack a { + color: #737b85 !important; +} +body.dark-mode .table-cell-stack a:hover { + color: #2b7fff !important; +} +.text-red-500 { + color: #cb2431; +} +.table-cell-status { + text-align: center; +} +.table-cell-status, .server-column { + cursor: default; +} +.status-running { + color: #28a745; + font-weight: 400; +} +.status-healthy { + color: #28a745; + font-weight: 400; +} +.status-exited { + color: #dc3545; + font-weight: 400; +} +.status-unhealthy { + color: #dc3545; + font-weight: 400; +} +.status-dead { + color: #dc3545; + font-weight: 400; +} +.status-unknown { + color: #dc3545; + font-weight: 400; +} +.status-starting { + color: #99a1af; + font-weight: 400; +} +.status-restarting { + color: #99a1af; + font-weight: 400; +} +.status-removing { + color: #99a1af; + font-weight: 400; +} +.status-paused { + color: #fd7e14; + font-weight: 400; +} +.status-created { + color: #99a1af; + font-weight: 400; +} +.status-none { + color: #99a1af; + font-weight: 400; +} +code { + background-color: #f3f4f6; + color: #24292e; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; +} +.filter-button { + padding: 0.4rem 1rem; + border-radius: 0.5rem; + background-color: #e1e4e8; + color: #3e4043; + font-weight: 500; + cursor: pointer; + box-shadow: 0.2s ease-in-out; +} +.filter-button:hover { + background-color: #e9eaeb; +} +.filter-button.active { + background-color: #2188ff; + color: white; +} +.filter-button.inactive { + color: #d2d3d3; + background-color: #f3f3f3; +} +body.dark-mode .filter-button { + background-color: #232831; + color: #b3b9bf; +} +body.dark-mode .filter-button:hover { + background-color: #202329; +} +body.dark-mode .filter-button.active { + background-color: #2188ff; + color: white; +} +body.dark-mode .filter-button.inactive { + color: #30353d; + background-color: #15181c; +} +.table-single-server .server-column { + display: none; +} +body.dark-mode { + background-color: #0d1117; + color: #c9d1d9; +} +body.dark-mode .container { + background-color: #161b22; +} +body.dark-mode .text-gray-800 { + color: #24292e; +} +body.dark-mode .text-gray-600 { + color: #8b949e; +} +body.dark-mode .text-gray-700 { + color: #c9d1d9; +} +body.dark-mode .text-gray-900 { + color: #c9d1d9; +} +body.dark-mode .bg-gray-100 { + background-color: #21262d; +} +body.dark-mode .border-gray-200 { + border-color: #232831; +} +body.dark-mode table { + background-color: #161b22; + border-color: #232831; +} +.main-table-header { + background-color: #f7f7f7; + color: #c7c7c7; +} +body.dark-mode .main-table-header { + background-color: #21262d; + color: #c7c7c7; +} +body.dark-mode .hover\:bg-gray-50:hover { + background-color: #21262d !important; +} +body.dark-mode .text-muted, body.dark-mode .text-secondary { + color: #8b949e; +} +body.dark-mode .sortable-header::after { + border-bottom-color: #c9d1d9; + border-top-color: #c9d1d9; +} +body.dark-mode input[type="text"] { + background-color: #0d1117; + border-color: #30363d; + color: #aab0b5; +} +body.dark-mode #search-input::placeholder { + color: #41464b; +} +body.dark-mode #theme-switcher, body.dark-mode #refresh-button { + background-color: #21262d; + color: #97a1ab; +} +body.dark-mode #theme-switcher:hover, body.dark-mode #refresh-button:hover { + background-color: #30363d; +} +body.dark-mode .text-red-500 { + color: #f85149; +} +body.dark-mode code { + background-color: #21262d; + color: #c9d1d9; +} +.modal-overlay:not(.hidden) { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(4px); +} +.modal-content:not(.hidden) { + background-color: #ffffff; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 700px; + max-height: 80vh; + text-align: center; + display: flex; + flex-direction: column; +} +body.dark-mode .modal-content { + background-color: #161b22; + border: 1px solid #30363d; +} +body.dark-mode #modal-cancel-button { + background-color: #21262d; + color: #c9d1d9; +} +body.dark-mode #modal-cancel-button:hover { + background-color: #30363d; +} +#updates-modal .modal-content:not(.hidden) { + max-width: 900px !important; +} +#updates-modal-message { + margin-bottom: 1.5rem; + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +#updates-list { + background-color: #f8f9fa; + padding: 1rem; + border-radius: 0.375rem; + border: 1px solid #e1e4e8; + max-height: 500px; + overflow-y: auto; + flex: 1; + min-height: 100px; +} +#updates-list li { + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + padding: 0.5rem 0; + border-bottom: 1px solid #e1e4e8; +} +#updates-list li:last-child { + border-bottom: none; +} +#updates-list .stack-name { + color: #a6b4c3; + font-weight: normal; + font-size: 0.857rem; + flex-shrink: 0; +} +#updates-list .server-name { + color: #a1aab4; + font-weight: normal; + font-size: 0.875rem; + flex-shrink: 0; +} +#updates-list .image-name { + color: #a6b4c3; + font-style: italic; + font-size: 0.8rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + text-align: left; + min-width: 0; +} +body.dark-mode #updates-list { + background-color: #21262d; + border-color: #30363d; +} +body.dark-mode #updates-list li { + border-bottom-color: #30363d; +} +body.dark-mode #updates-list .server-name { + color: #6a737d; +} +body.dark-mode #updates-list .image-name { + color: #6a737d; +} +#updates-list .no-updates-message { + list-style: none; + color: #28a745; + font-weight: 600; + font-size: 1rem; + white-space: normal; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + padding: 2rem 1rem; +} +body.dark-mode #updates-list .no-updates-message { + color: #56d364; +} +#updates-list::-webkit-scrollbar { + width: 8px; +} +#updates-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} +#updates-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} +#updates-list::-webkit-scrollbar-thumb:hover { + background: #a1a1a1; +} +body.dark-mode #updates-list::-webkit-scrollbar-track { + background: #30363d; +} +body.dark-mode #updates-list::-webkit-scrollbar-thumb { + background: #6a737d; +} +body.dark-mode #updates-list::-webkit-scrollbar-thumb:hover { + background: #8b949e; +} +.loader { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 2rem auto; +} +body.dark-mode .loader { + border-color: #21262d; + border-top-color: #2188ff; +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +@keyframes spin-reverse { + from { + transform: rotate(0deg); + } + to { + transform: rotate(-360deg); + } +} +.update-indicator:not(.hidden) { + display: inline-flex; + align-items: center; +} +.update-indicator svg { + width: 20px; + height: 20px; + color: #d67a10; +} +body.dark-mode .update-indicator svg { + color: #b9711f; +} +.image-wrapper { + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-width: 200px; +} +.image-wrapper-inline { + display: inline; +} +.table-cell-image code { + display: inline; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + background-color: #ededed; + color: #3e4043; + border-radius: 0.25rem; + padding: 0.2rem 0.4rem; + white-space: break-spaces; + word-break: break-all; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} +body.dark-mode .table-cell-image code { + background-color: #22262d; + color: #d5d5d5; +} +#check-updates-button { + background-color: #e1e4e8; + color: #626c78; + padding: 0.5rem 1rem; + border-radius: 0.5rem; +} +#check-updates-button:hover { + background-color: #d1d5da; +} +#check-updates-button svg { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-right: 0.5rem; +} +#check-updates-button.loading svg { + display: none; +} +#check-updates-button.loading::before { + content: ""; + display: inline-block; + border: 2px solid #f3f3f3; + border-top: 2px solid #d67a10; + border-radius: 50%; + width: 18px; + height: 18px; + animation: spin 1s linear infinite; + vertical-align: middle; + margin-right: 0.5rem; +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +body.dark-mode #check-updates-button { + background-color: #21262d; + color: #97a1ab; +} +body.dark-mode #check-updates-button:hover { + background-color: #30363d; +} +body.dark-mode #check-updates-button.loading::before { + border: 2px solid #30363d; + border-top: 2px solid #b9711f; +} +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; +} +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #e1e4e8; + border-radius: 24px; +} +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + transition: transform 0.1s ease-in-out; +} +input:checked+.slider { + background-color: #3f88fe; +} +input:checked+.slider:before { + transform: translateX(20px); +} +body.dark-mode .slider { + background-color: #33393f; +} +body.dark-mode .slider:before { + background-color: #181c25; +} +body.dark-mode input:checked+.slider { + background-color: #567bd4; +} +#filter-updates-checkbox:checked+.slider { + background-color: #d67a10; +} +body.dark-mode #filter-updates-checkbox:checked+.slider { + background-color: #d67a10; +} +#filter-running-checkbox:checked+.slider { + background-color: #3ec93c; +} +body.dark-mode #filter-running-checkbox:checked+.slider { + background-color: #3b8531; +} +label[for="filter-running-checkbox"] { + color: #babdc1; +} +body.dark-mode label[for="filter-running-checkbox"] { + color: #4d525b; +} +#modal-message { + white-space: pre-line; + line-height: 1.6; +} +button:focus { + outline: none !important; + box-shadow: none !important; +} +.traefik-route a { + color: #3e71e9 !important; +} +.traefik-route a:hover { + color: #2b7fff !important; +} +body.dark-mode .traefik-route a { + color: #2572cb !important; +} +body.dark-mode .traefik-route a:hover { + color: #2b7fff !important; +} +.column-menu-container { + margin-left: 16px; + position: relative; + display: inline-block; +} +.column-menu-reset { + text-align: right; +} +.reset-columns-btn { + display: inline-block; + color: #767983; + font-weight: normal; + padding-top: 0.75em; + padding-right: 0.3em; +} +.column-menu-container { + height: 32px; + width: 32px; + cursor: pointer; + border-radius: 0.375rem; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} +.column-menu-svg { + height: 26px; + width: 26px; + color: #c0c4c9; + pointer-events: none; +} +.column-menu-container:hover .column-menu-svg { + color: #a2a6aa; +} +body.dark-mode .column-menu-svg { + color: #767e87; +} +body.dark-mode .column-menu-container:hover .column-menu-svg { + color: #40464d; +} +.column-menu { + position: absolute; + top: calc(100% + 0px); + right: 0; + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 0.5rem; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1); + padding: 0.75rem; + min-width: 180px; + z-index: 1000; +} +.column-menu-item { + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + font-size: 0.875rem; +} +.column-menu-item:not(:last-child) { + border-bottom: 1px solid #f1f3f4; +} +.column-menu-item span { + color: #24292e; + font-weight: 400; +} +body.dark-mode .column-menu-button { + background-color: #21262d; + color: #97a1ab; +} +body.dark-mode .column-menu-button:hover { + background-color: #30363d; +} +body.dark-mode .column-menu { + background-color: #1c2129; + border-color: #282d33; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.3); +} +body.dark-mode .column-menu-item { + border-bottom-color: #282d33; +} +body.dark-mode .column-menu-item span { + color: #c9d1d9; +} +.column-hidden { + display: none !important; +} +@media (max-width: 576px) { + body { + padding: 0; + } + #search-input::placeholder { + opacity: 0; + } + .container { + padding: 0.5rem; + } + .flex.justify-between.items-center.mb-6 { + flex-direction: column; + align-items: flex-start; + margin-bottom: 1rem; + } + .controls-container { + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 1rem; + } + .controls-container>* { + margin-bottom: 0.5rem; + width: 100%; + } + .py-3.px-4 { + padding: 0.5rem 0.75rem; + } + .min-w-full { + min-width: unset; + } + .update-indicator { + display: none; + } + .min-w-full { + min-width: 100%; + } +} +@media (max-width: 768px) { + .container { + padding: 0.5rem; + } + .column-menu-container { + margin-left: 8px; + } + .right-18 { + right: calc(var(--spacing) * 14); + } + [data-tooltip-left]::after, [data-tooltip-left]::before, [data-tooltip-right]::after, [data-tooltip-right]::before, [data-tooltip]::after, [data-tooltip]::before { + display: none !important; + content: none !important; + } + .flex.justify-between.items-center.mb-6 { + flex-direction: column; + align-items: flex-start; + margin-bottom: 1rem; + } + .status-filter { + display: flex; + justify-content: right; + align-items: center; + height: 100%; + } + .column-menu-container { + display: flex; + justify-content: right; + align-items: center; + height: 100%; + } + .controls-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 1rem; + } + .controls-container>* { + margin-bottom: 0.5rem; + width: 100%; + } + .py-3.px-4 { + padding: 0.5rem 0.75rem; + } + .min-w-full { + min-width: unset; + } + .min-w-full { + min-width: 100%; + } +} +.traefik-route a { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.traefik-route a .traefik-text { + display: inline; +} +label[for="filter-running-checkbox"] { + display: inline; +} +@media (max-width: 1023px) { + label[for="filter-running-checkbox"] { + display: none; + } +} +@media (max-width: 768px) { + label[for="filter-running-checkbox"] { + display: inline; + } +} +@media (min-width: 1024px) { + label[for="filter-running-checkbox"] { + display: inline; + } +} +.tag-badge { + max-width: 120px; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + background-color: #ededed; + color: #727373; + font-size: 0.75rem; + font-weight: 400; + padding: 2px 7px; + border-radius: 9999px; + cursor: pointer; +} +.tag-badge:hover { + background-color: #e1e4e8; +} +.dark-mode .tag-badge { + background-color: #22262d; + color: #90939b; +} +.dark-mode .tag-badge:hover { + background-color: #2d3137; +} +.tags-container { + display: inline-flex; + flex-wrap: wrap; + gap: 4px; + max-width: 250px; +} +.column-menu { + min-width: 200px; +} +.column-menu-header { + padding: 8px 12px; + border-bottom: 1px solid #e5e7eb; + margin-bottom: 8px; +} +.column-list { + max-height: 400px; + overflow-y: auto; +} +.column-menu-item.draggable { + display: flex; + align-items: center; + cursor: move; + padding: 8px 4px; + border: 1px solid transparent; + border-radius: 4px; + margin: 2px 8px; +} +.column-menu-item.dragging { + opacity: 0.5; + background-color: #f3f4f6; + border-color: #d1d5db; +} +.dark-mode .column-menu-item.dragging { + opacity: 0.5; + background-color: #454850; + border-color: #373b44; +} +.column-menu-item.drag-over { + border-top: 2px solid #3b82f6; +} +.drag-handle { + color: #9ca3af; + font-weight: bold; + margin-right: 8px; + user-select: none; + line-height: 1; +} +.drag-handle:hover { + color: #6b7280; +} +.column-menu-item label { + flex: 1; + cursor: pointer; + display: flex; + justify-content: space-between; +} +.custom-tooltip { + position: absolute; + z-index: 1000; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; +} +.custom-tooltip.is-visible { + opacity: 1; + visibility: visible; +} +.custom-tooltip-box { + background-color: #ffffff; + color: #4f5359; + font-size: 0.8rem; + font-weight: 400; + padding: 0.4rem 0.8rem; + border-radius: 0.375rem; + white-space: nowrap; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.2); + max-width: 600px; + overflow: hidden; + text-overflow: ellipsis; +} +.custom-tooltip-arrow { + content: ""; + position: absolute; + width: 0; + height: 0; + border-style: solid; +} +.custom-tooltip-arrow.arrow-top { + border-width: 6px 6px 0 6px; + border-color: #ffffff transparent transparent transparent; + left: 50%; + top: 100%; + transform: translateX(-50%); +} +.custom-tooltip-arrow.arrow-left { + border-width: 10px 0 10px 10px; + border-color: transparent transparent transparent #ffffff; + top: 50%; + left: 100%; + transform: translateY(-50%); +} +.custom-tooltip-arrow.arrow-right { + border-width: 10px 10px 10px 0; + border-color: transparent #ffffff transparent transparent; + top: 50%; + right: 100%; + transform: translateY(-50%); +} +body.dark-mode .custom-tooltip-box { + background-color: #31363d; + color: #f6f8fa; + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.4); +} +body.dark-mode .custom-tooltip-arrow.arrow-top { + border-top-color: #31363d; +} +body.dark-mode .custom-tooltip-arrow.arrow-left { + border-left-color: #31363d; +} +body.dark-mode .custom-tooltip-arrow.arrow-right { + border-right-color: #31363d; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-x-reverse: 0; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + --tw-ease: initial; + } + } +} diff --git a/static_old/fonts/inter/inter-v19-latin-500.woff2 b/static_old/fonts/inter/inter-v19-latin-500.woff2 new file mode 100644 index 0000000..54f0a59 Binary files /dev/null and b/static_old/fonts/inter/inter-v19-latin-500.woff2 differ diff --git a/static_old/fonts/inter/inter-v19-latin-600.woff2 b/static_old/fonts/inter/inter-v19-latin-600.woff2 new file mode 100644 index 0000000..d189794 Binary files /dev/null and b/static_old/fonts/inter/inter-v19-latin-600.woff2 differ diff --git a/static_old/fonts/inter/inter-v19-latin-700.woff2 b/static_old/fonts/inter/inter-v19-latin-700.woff2 new file mode 100644 index 0000000..48fa217 Binary files /dev/null and b/static_old/fonts/inter/inter-v19-latin-700.woff2 differ diff --git a/static_old/fonts/inter/inter-v19-latin-800.woff2 b/static_old/fonts/inter/inter-v19-latin-800.woff2 new file mode 100644 index 0000000..74a16d4 Binary files /dev/null and b/static_old/fonts/inter/inter-v19-latin-800.woff2 differ diff --git a/static_old/fonts/inter/inter-v19-latin-regular.woff2 b/static_old/fonts/inter/inter-v19-latin-regular.woff2 new file mode 100644 index 0000000..f15b025 Binary files /dev/null and b/static_old/fonts/inter/inter-v19-latin-regular.woff2 differ diff --git a/static_old/fonts/unused/fonts.css b/static_old/fonts/unused/fonts.css new file mode 100644 index 0000000..6ad2262 --- /dev/null +++ b/static_old/fonts/unused/fonts.css @@ -0,0 +1,144 @@ +/* inter-100 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + src: url('/static/fonts/inter/inter-v19-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-100italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 100; + src: url('/static/fonts/inter/inter-v19-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-200 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + src: url('/static/fonts/inter/inter-v19-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-200italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 200; + src: url('/static/fonts/inter/inter-v19-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-300 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + src: url('/static/fonts/inter/inter-v19-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-300italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 300; + src: url('/static/fonts/inter/inter-v19-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url('/static/fonts/inter/inter-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + src: url('/static/fonts/inter/inter-v19-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-500 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('/static/fonts/inter/inter-v19-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-500italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + src: url('/static/fonts/inter/inter-v19-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + src: url('/static/fonts/inter/inter-v19-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-600italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + src: url('/static/fonts/inter/inter-v19-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-700 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url('/static/fonts/inter/inter-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-700italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + src: url('/static/fonts/inter/inter-v19-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-800 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + src: url('/static/fonts/inter/inter-v19-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-800italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 800; + src: url('/static/fonts/inter/inter-v19-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-900 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + src: url('/static/fonts/inter/inter-v19-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* inter-900italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Inter'; + font-style: italic; + font-weight: 900; + src: url('/static/fonts/inter/inter-v19-latin-900italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} \ No newline at end of file diff --git a/static_old/fonts/unused/inter-v19-latin-100.woff2 b/static_old/fonts/unused/inter-v19-latin-100.woff2 new file mode 100644 index 0000000..fbcfb9e Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-100.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-100italic.woff2 b/static_old/fonts/unused/inter-v19-latin-100italic.woff2 new file mode 100644 index 0000000..8427f11 Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-100italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-200.woff2 b/static_old/fonts/unused/inter-v19-latin-200.woff2 new file mode 100644 index 0000000..37267df Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-200.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-200italic.woff2 b/static_old/fonts/unused/inter-v19-latin-200italic.woff2 new file mode 100644 index 0000000..cb15f8d Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-200italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-300.woff2 b/static_old/fonts/unused/inter-v19-latin-300.woff2 new file mode 100644 index 0000000..ece952c Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-300.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-300italic.woff2 b/static_old/fonts/unused/inter-v19-latin-300italic.woff2 new file mode 100644 index 0000000..dd92d3b Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-300italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-500italic.woff2 b/static_old/fonts/unused/inter-v19-latin-500italic.woff2 new file mode 100644 index 0000000..f4f25da Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-500italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-600italic.woff2 b/static_old/fonts/unused/inter-v19-latin-600italic.woff2 new file mode 100644 index 0000000..e882c78 Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-600italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-700italic.woff2 b/static_old/fonts/unused/inter-v19-latin-700italic.woff2 new file mode 100644 index 0000000..b6a7cad Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-700italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-800italic.woff2 b/static_old/fonts/unused/inter-v19-latin-800italic.woff2 new file mode 100644 index 0000000..e98fa7e Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-800italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-900.woff2 b/static_old/fonts/unused/inter-v19-latin-900.woff2 new file mode 100644 index 0000000..4db8333 Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-900.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-900italic.woff2 b/static_old/fonts/unused/inter-v19-latin-900italic.woff2 new file mode 100644 index 0000000..291eafc Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-900italic.woff2 differ diff --git a/static_old/fonts/unused/inter-v19-latin-italic.woff2 b/static_old/fonts/unused/inter-v19-latin-italic.woff2 new file mode 100644 index 0000000..9e98286 Binary files /dev/null and b/static_old/fonts/unused/inter-v19-latin-italic.woff2 differ diff --git a/static_old/js/app.js b/static_old/js/app.js new file mode 100644 index 0000000..daa7b97 --- /dev/null +++ b/static_old/js/app.js @@ -0,0 +1,1453 @@ +document.addEventListener("DOMContentLoaded", () => { + let allContainersData = []; + let allServersData = []; + let filteredAndSortedContainers = []; + let currentSortColumn = "name"; + let currentSortDirection = "asc"; + let currentServerFilter = "all"; + let isDataLoaded = false; + let columnOrder = ['name', 'stack', 'server', 'ports', 'traefik', 'image', 'tags', 'status']; + let columnVisibility = { + name: true, + server: true, + stack: true, + image: true, + tags: true, + status: true, + ports: true, + traefik: true + }; + + const searchInput = document.getElementById("search-input"); + const clearSearchButton = document.getElementById("clear-search-button"); + const containerRowsBody = document.getElementById("container-rows"); + const body = document.body; + const rowTemplate = document.getElementById("container-row-template"); + const serverFilterContainer = document.getElementById("server-filter-container"); + const mainTable = document.getElementById("main-table"); + const refreshButton = document.getElementById('refresh-button'); + const checkUpdatesButton = document.getElementById('check-updates-button'); + checkUpdatesButton.disabled = true; + const updatesModal = document.getElementById("updates-modal"); + const updatesModalOkBtn = document.getElementById("updates-modal-ok-button"); + const modal = document.getElementById("confirmation-modal"); + const modalConfirmBtn = document.getElementById("modal-confirm-button"); + const modalCancelBtn = document.getElementById("modal-cancel-button"); + const filterUpdatesCheckbox = document.getElementById("filter-updates-checkbox"); + const filterRunningCheckbox = document.getElementById("filter-running-checkbox"); + + function showLoadingIndicator() { + refreshButton.classList.add('loading'); + containerRowsBody.innerHTML = `
`; + } + + function hideLoadingIndicator() { + refreshButton.classList.remove('loading'); + } + + function displayError(message) { + hideLoadingIndicator(); + containerRowsBody.innerHTML = `${message}`; + } + + function initCustomTooltips() { + let tooltipContainer = document.getElementById('tooltip-container'); + if (!tooltipContainer) { + tooltipContainer = document.createElement('div'); + tooltipContainer.id = 'tooltip-container'; + document.body.appendChild(tooltipContainer); + } + + let currentTooltip = null; + let hideTooltipTimer = null; + + function showTooltip(text, element, type) { + clearTimeout(hideTooltipTimer); + if (currentTooltip && currentTooltip.dataset.owner === element) { + return; + } + + if (currentTooltip) { + hideTooltip(true); + } + + const tooltipElement = document.createElement('div'); + tooltipElement.className = 'custom-tooltip'; + tooltipElement.dataset.owner = element; + + const tooltipBox = document.createElement('div'); + tooltipBox.className = 'custom-tooltip-box'; + tooltipBox.textContent = text; + + const tooltipArrow = document.createElement('div'); + tooltipArrow.className = 'custom-tooltip-arrow'; + + tooltipElement.appendChild(tooltipBox); + tooltipElement.appendChild(tooltipArrow); + tooltipContainer.appendChild(tooltipElement); + + currentTooltip = tooltipElement; + tooltipElement.addEventListener('mouseover', () => clearTimeout(hideTooltipTimer)); + tooltipElement.addEventListener('mouseout', () => hideTooltip()); + + + const rect = element.getBoundingClientRect(); + const tooltipRect = tooltipElement.getBoundingClientRect(); + let top, left; + const margin = 10; + + switch (type) { + case 'data-tooltip-left': + tooltipArrow.classList.add('arrow-right'); + top = rect.top + window.scrollY + (rect.height / 2) - (tooltipRect.height / 2); + left = rect.left + window.scrollX - tooltipRect.width - margin; + break; + case 'data-tooltip-right': + tooltipArrow.classList.add('arrow-left'); + top = rect.top + window.scrollY + (rect.height / 2) - (tooltipRect.height / 2); + left = rect.right + window.scrollX + margin; + break; + case 'data-tooltip-top-right': + tooltipArrow.classList.add('arrow-top'); + tooltipArrow.style.left = `${rect.width / 2}px`; + tooltipArrow.style.transform = 'translateX(-50%)'; + top = rect.top + window.scrollY - tooltipRect.height - margin; + left = rect.right + window.scrollX - tooltipRect.width; + break; + case 'data-tooltip-top-left': + tooltipArrow.classList.add('arrow-top'); + tooltipArrow.style.left = `${rect.width / 2}px`; + tooltipArrow.style.transform = 'translateX(-50%)'; + top = rect.top + window.scrollY - tooltipRect.height - margin; + left = rect.left + window.scrollX; + break; + default: + tooltipArrow.classList.add('arrow-top'); + top = rect.top + window.scrollY - tooltipRect.height - margin; + left = rect.left + window.scrollX + (rect.width / 2) - (tooltipRect.width / 2); + break; + } + + tooltipElement.style.top = `${top}px`; + tooltipElement.style.left = `${left}px`; + + requestAnimationFrame(() => { + tooltipElement.classList.add('is-visible'); + }); + } + + // NOWOŚĆ: Funkcja przyjmuje opcjonalny argument `immediate` + function hideTooltip(immediate = false) { + clearTimeout(hideTooltipTimer); // Zawsze czyść poprzedni timer + if (!currentTooltip) return; + + if (immediate) { + if (currentTooltip.parentElement) { + currentTooltip.remove(); + } + currentTooltip = null; + } else { + hideTooltipTimer = setTimeout(() => { + if (!currentTooltip) return; + const tooltipToRemove = currentTooltip; + currentTooltip = null; + tooltipToRemove.classList.remove('is-visible'); + tooltipToRemove.addEventListener('transitionend', () => { + if (tooltipToRemove.parentElement) { + tooltipToRemove.remove(); + } + }, { once: true }); + }, 100); // Opóźnienie 100ms przed ukryciem + } + } + + const tooltipAttributes = [ + 'data-tooltip', + 'data-tooltip-left', + 'data-tooltip-right', + 'data-tooltip-top-left', + 'data-tooltip-top-right' + ]; + + document.addEventListener('mouseover', (e) => { + for (const attr of tooltipAttributes) { + const target = e.target.closest(`[${attr}]`); + if (target) { + const tooltipText = target.getAttribute(attr); + if (tooltipText) { + showTooltip(tooltipText, target, attr); + return; + } + } + } + }); + + document.addEventListener('mouseout', (e) => { + const isTooltipTarget = tooltipAttributes.some(attr => e.target.closest(`[${attr}]`)); + if (isTooltipTarget) { + hideTooltip(); + } + }); + + document.addEventListener('scroll', () => hideTooltip(true), true); + } + + + function renderTable() { + containerRowsBody.innerHTML = ""; + const pageItems = filteredAndSortedContainers; + + if (pageItems.length === 0) { + containerRowsBody.innerHTML = `No containers found matching your criteria.`; + return; + } + + + const fragment = document.createDocumentFragment(); + for (const c of pageItems) { + const clone = rowTemplate.content.cloneNode(true); + + + const nameCell = clone.querySelector('[data-content="name"]'); + nameCell.classList.add('table-cell-name'); + + const nameSpan = nameCell.querySelector('[data-content="container-name"]'); + const tagsContainer = nameCell.querySelector('[data-content="tags"]'); + + if (c.custom_url) { + function normalizeUrl(url) { + if (url.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) { + return url; + } + return `https://${url}`; + } + + const url = normalizeUrl(c.custom_url); + const tooltipUrl = url.replace(/^https:\/\//, ''); + nameSpan.innerHTML = `${c.name}`; + } else { + nameSpan.textContent = c.name; + } + + // Add tags display (Name column) + // if (c.tags && c.tags.length > 0) { + // tagsContainer.innerHTML = c.tags.map(tag => + // `${tag}` + // ).join(''); + // } else { + // tagsContainer.innerHTML = ''; + // } + const serverNameSpan = clone.querySelector('[data-content="server-name"]'); + serverNameSpan.closest('td').classList.add('table-cell-server'); + serverNameSpan.textContent = c.server; + const serverData = allServersData.find(s => s.name === c.server); + if (serverData && serverData.url) { + serverNameSpan.setAttribute('data-tooltip', serverData.url); + } + + // Stack column - make clickable if stack exists + const stackCell = clone.querySelector('[data-content="stack"]'); + stackCell.classList.add('table-cell-stack'); + if (c.stack) { + stackCell.innerHTML = `${c.stack}`; + } else { + stackCell.textContent = ''; + } + + + + clone.querySelector('[data-content="image"]').textContent = c.image; + clone.querySelector('[data-content="image"]').closest('td').classList.add('table-cell-image'); + + // Source link handling + const sourceLink = clone.querySelector('[data-content="source-link"]'); + if (c.source_url) { + sourceLink.href = c.source_url; + sourceLink.classList.remove('hidden'); + sourceLink.setAttribute('data-tooltip', c.source_url); + } else { + sourceLink.classList.add('hidden'); + } + + const updateIndicator = clone.querySelector('[data-content="update-indicator"]'); + if (c.update_available) { + updateIndicator.classList.remove('hidden'); + updateIndicator.setAttribute('data-tooltip', 'Update available'); + } else { + updateIndicator.classList.add('hidden'); + } + + const tagsCell = clone.querySelector('[data-content="tags"]'); + tagsCell.classList.add('table-cell-tags'); + if (c.tags && c.tags.length > 0) { + const sortedTags = [...c.tags].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + tagsCell.innerHTML = `
${sortedTags.map(tag => + `${tag}` + ).join('')}
`; + } else { + tagsCell.innerHTML = ''; + } + + const statusCell = clone.querySelector('[data-content="status"]'); + const statusSpan = document.createElement('span'); + statusSpan.textContent = c.status; + + if (c.exit_code !== null && c.exit_code !== undefined) { + let exitCodeText; + if (c.exit_code === 0) { + exitCodeText = 'Exit code: 0 (normal)'; + } else { + exitCodeText = `Exit code: ${c.exit_code}`; + if (c.exit_code === 137) exitCodeText += ' (SIGKILL - killed)'; + else if (c.exit_code === 143) exitCodeText += ' (SIGTERM - terminated)'; + else if (c.exit_code === 125) exitCodeText += ' (Docker daemon error)'; + else if (c.exit_code === 126) exitCodeText += ' (Container command not executable)'; + else if (c.exit_code === 127) exitCodeText += ' (Container command not found)'; + else if (c.exit_code === 1) exitCodeText += ' (General application error)'; + else if (c.exit_code === 2) exitCodeText += ' (Misuse of shell command)'; + else if (c.exit_code === 128) exitCodeText += ' (Invalid exit argument)'; + else if (c.exit_code === 130) exitCodeText += ' (SIGINT - interrupted)'; + else if (c.exit_code === 134) exitCodeText += ' (SIGABRT - aborted)'; + else if (c.exit_code === 139) exitCodeText += ' (SIGSEGV - segmentation fault)'; + } + statusSpan.setAttribute('data-tooltip', exitCodeText); + } else { + let tooltipText; + switch (c.status) { + case 'running': + tooltipText = 'Container is running'; + break; + case 'healthy': + tooltipText = 'Health check passed'; + break; + case 'unhealthy': + tooltipText = 'Health check failed'; + break; + case 'starting': + tooltipText = 'Container is starting up'; + break; + case 'paused': + tooltipText = 'Container is paused'; + break; + case 'restarting': + tooltipText = 'Container is restarting'; + break; + case 'removing': + tooltipText = 'Container is being removed'; + break; + case 'dead': + tooltipText = 'Container is dead (cannot be restarted)'; + break; + case 'created': + tooltipText = 'Container created but not started'; + break; + default: + if (c.status.includes('health unknown')) { + tooltipText = 'Container running, health status unknown'; + } else { + tooltipText = `Container status: ${c.status}`; + } + } + statusSpan.setAttribute('data-tooltip', tooltipText); + } + + let statusClass = 'status-unknown'; + + switch (c.status) { + case 'running': + statusClass = 'status-running'; + break; + case 'healthy': + statusClass = 'status-healthy'; + break; + case 'unhealthy': + statusClass = 'status-unhealthy'; + break; + case 'starting': + statusClass = 'status-starting'; + break; + case 'exited': + statusClass = 'status-exited'; + break; + case 'paused': + statusClass = 'status-paused'; + break; + case 'restarting': + statusClass = 'status-restarting'; + break; + case 'removing': + statusClass = 'status-removing'; + break; + case 'dead': + statusClass = 'status-dead'; + break; + case 'created': + statusClass = 'status-created'; + break; + default: + if (c.status.includes('exited')) { + statusClass = 'status-exited'; + } else if (c.status.includes('health unknown')) { + statusClass = 'status-running'; + } else { + statusClass = 'status-unknown'; + } + } + + statusCell.className = `py-3 px-4 border-b border-gray-200 table-cell-status ${statusClass}`; + statusCell.innerHTML = ''; + statusCell.appendChild(statusSpan); + + const portsCell = clone.querySelector('[data-content="ports"]'); + portsCell.classList.add('table-cell-ports'); + if (c.ports.length > 0) { + const arrowSvg = + ` + + `; + + portsCell.innerHTML = c.ports.map(p => { + // Custom ports (from dockpeek.ports label) + if (p.is_custom || (!p.container_port || p.container_port === '')) { + return ``; + } else { + // Standard ports with mapping + return `
+ ${p.host_port} + ${arrowSvg} + ${p.container_port} +
`; + } + }).join(''); + } else { + portsCell.innerHTML = `none`; + } + + // Check if Traefik is enabled globally and if any container has routes + const isTraefikGloballyEnabled = window.traefikEnabled !== false; + const hasTraefikRoutes = isTraefikGloballyEnabled && pageItems.some(c => c.traefik_routes && c.traefik_routes.length > 0); + + // Traefik routes handling + const traefikCell = clone.querySelector('[data-content="traefik-routes"]'); + traefikCell.classList.add('table-cell-traefik'); + if (hasTraefikRoutes) { + traefikCell.classList.remove('hidden'); + + if (c.traefik_routes && c.traefik_routes.length > 0) { + traefikCell.innerHTML = c.traefik_routes.map(route => { + const displayUrl = route.url.replace(/^https?:\/\//, ''); + return ``; + + }).join(''); + } else { + traefikCell.innerHTML = `none`; + } + } else { + traefikCell.classList.add('hidden'); + } + fragment.appendChild(clone); + + } + containerRowsBody.appendChild(fragment); + updateTableColumnOrder(); + updateColumnVisibility(); + + } + + // Column visibility functionality (CORRECTED VERSION) + const columnMenuButton = document.getElementById('column-menu-button'); + const columnMenu = document.getElementById('column-menu'); + + // Reset columns button functionality + const resetColumnsButton = document.getElementById('reset-columns-button'); + if (resetColumnsButton) { + resetColumnsButton.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent menu from closing + + console.log('Resetting all columns to visible'); + + // Reset column visibility + Object.keys(columnVisibility).forEach(column => { + columnVisibility[column] = true; + const toggle = document.getElementById(`toggle-${column}`); + if (toggle) { + toggle.checked = true; + } + }); + + // Reset column order + columnOrder = ['name', 'stack', 'server', 'ports', 'traefik', 'image', 'tags', 'status']; + reorderColumnMenuItems(); + saveColumnOrder(); + updateTableColumnOrder(); + + // Save to localStorage + localStorage.setItem('columnVisibility', JSON.stringify(columnVisibility)); + + // Apply changes + updateColumnVisibility(); + + console.log('Columns reset complete:', columnVisibility); + }); + } + // Load column visibility from localStorage + const savedVisibility = localStorage.getItem('columnVisibility'); + if (savedVisibility) { + columnVisibility = JSON.parse(savedVisibility); + } + + // Apply saved visibility and update toggles + Object.keys(columnVisibility).forEach(column => { + const toggle = document.getElementById(`toggle-${column}`); + if (toggle) { + toggle.checked = columnVisibility[column]; + } + }); + + // Toggle menu visibility + columnMenuButton.addEventListener('click', (e) => { + e.stopPropagation(); + columnMenu.classList.toggle('hidden'); + }); + + // Close menu when clicking outside + document.addEventListener('click', () => { + columnMenu.classList.add('hidden'); + }); + + columnMenu.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // Handle column toggle changes - FIXED VERSION + Object.keys(columnVisibility).forEach(column => { + const toggle = document.getElementById(`toggle-${column}`); + if (toggle) { + toggle.addEventListener('change', () => { + columnVisibility[column] = toggle.checked; + localStorage.setItem('columnVisibility', JSON.stringify(columnVisibility)); + updateColumnVisibility(); + }); + } + }); + + function updateColumnVisibility() { + // Update table headers + document.querySelectorAll(`[data-sort-column="name"]`).forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.name); + }); + + // Fix server column selector - use class instead of data attribute + document.querySelectorAll('.server-column').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.server); + }); + + document.querySelectorAll(`[data-sort-column="stack"]`).forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.stack); + }); + + document.querySelectorAll(`[data-sort-column="image"]`).forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.image); + }); + + document.querySelectorAll(`[data-sort-column="tags"]`).forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.tags); + }); + + document.querySelectorAll(`[data-sort-column="status"]`).forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.status); + }); + + document.querySelectorAll(`[data-sort-column="ports"]`).forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.ports); + }); + + document.querySelectorAll('.traefik-column').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.traefik); + }); + + document.querySelectorAll('.table-cell-name').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.name); + }); + + document.querySelectorAll('.table-cell-server').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.server); + }); + + document.querySelectorAll('.table-cell-stack').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.stack); + }); + + document.querySelectorAll('.table-cell-image').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.image); + }); + + document.querySelectorAll('.table-cell-tags').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.tags); + }); + document.querySelectorAll('.table-cell-status').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.status); + }); + + document.querySelectorAll('.table-cell-ports').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.ports); + }); + + document.querySelectorAll('.table-cell-traefik').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.traefik); + }); + + const hasTags = filteredAndSortedContainers.some(c => c.tags && c.tags.length > 0); + document.querySelectorAll('.tags-column').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.tags || !hasTags); + }); + document.querySelectorAll('.table-cell-tags').forEach(el => { + el.classList.toggle('column-hidden', !columnVisibility.tags || !hasTags); + }); + } + + function initColumnDragAndDrop() { + const columnList = document.getElementById('column-list'); + let draggedElement = null; + let touchStartY = 0; + let touchCurrentY = 0; + let isDragging = false; + + // Load saved column order + const savedOrder = localStorage.getItem('columnOrder'); + if (savedOrder) { + columnOrder = JSON.parse(savedOrder); + reorderColumnMenuItems(); + } + + // Desktop drag events + columnList.addEventListener('dragstart', (e) => { + if (e.target.classList.contains('draggable')) { + draggedElement = e.target; + e.target.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/html', e.target.outerHTML); + } + }); + + columnList.addEventListener('dragend', (e) => { + if (e.target.classList.contains('draggable')) { + e.target.classList.remove('dragging'); + draggedElement = null; + } + }); + + columnList.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const afterElement = getDragAfterElement(columnList, e.clientY); + const dragging = columnList.querySelector('.dragging'); + + columnList.querySelectorAll('.drag-over').forEach(el => { + el.classList.remove('drag-over'); + }); + + if (afterElement == null) { + columnList.appendChild(dragging); + } else { + afterElement.classList.add('drag-over'); + columnList.insertBefore(dragging, afterElement); + } + }); + + columnList.addEventListener('drop', (e) => { + e.preventDefault(); + columnList.querySelectorAll('.drag-over').forEach(el => { + el.classList.remove('drag-over'); + }); + + updateColumnOrderFromDOM(); + saveColumnOrder(); + updateTableColumnOrder(); + }); + + // Touch events for mobile + columnList.addEventListener('touchstart', (e) => { + const target = e.target.closest('.draggable'); + if (target) { + draggedElement = target; + touchStartY = e.touches[0].clientY; + isDragging = false; + + // Add a small delay to distinguish between tap and drag + setTimeout(() => { + if (draggedElement) { + isDragging = true; + draggedElement.classList.add('dragging'); + } + }, 150); + } + }, { passive: false }); + + columnList.addEventListener('touchmove', (e) => { + if (!draggedElement || !isDragging) return; + + e.preventDefault(); + touchCurrentY = e.touches[0].clientY; + + const afterElement = getDragAfterElement(columnList, touchCurrentY); + + columnList.querySelectorAll('.drag-over').forEach(el => { + el.classList.remove('drag-over'); + }); + + if (afterElement == null) { + columnList.appendChild(draggedElement); + } else { + afterElement.classList.add('drag-over'); + columnList.insertBefore(draggedElement, afterElement); + } + }, { passive: false }); + + columnList.addEventListener('touchend', (e) => { + if (draggedElement) { + columnList.querySelectorAll('.drag-over').forEach(el => { + el.classList.remove('drag-over'); + }); + + if (isDragging) { + draggedElement.classList.remove('dragging'); + updateColumnOrderFromDOM(); + saveColumnOrder(); + updateTableColumnOrder(); + } + + draggedElement = null; + isDragging = false; + } + }); + + // Make items draggable for desktop + columnList.querySelectorAll('.draggable').forEach(item => { + item.draggable = true; + }); + } + + function getDragAfterElement(container, y) { + const draggableElements = [...container.querySelectorAll('.draggable:not(.dragging)')]; + + return draggableElements.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; + } + + function updateColumnOrderFromDOM() { + const items = document.querySelectorAll('#column-list .draggable'); + columnOrder = Array.from(items).map(item => item.dataset.column); + } + + function reorderColumnMenuItems() { + const columnList = document.getElementById('column-list'); + const items = Array.from(columnList.children); + + // Sort items based on columnOrder + items.sort((a, b) => { + const aIndex = columnOrder.indexOf(a.dataset.column); + const bIndex = columnOrder.indexOf(b.dataset.column); + return aIndex - bIndex; + }); + + // Reappend in new order + items.forEach(item => columnList.appendChild(item)); + } + + function saveColumnOrder() { + localStorage.setItem('columnOrder', JSON.stringify(columnOrder)); + } + + function updateTableColumnOrder() { + const thead = document.querySelector('#main-table thead tr'); + const headers = Array.from(thead.children); + + // Reorder headers + columnOrder.forEach(columnName => { + const header = headers.find(h => + h.dataset.sortColumn === columnName || + h.classList.contains(`${columnName}-column`) || + h.classList.contains(`table-cell-${columnName}`) + ); + if (header) { + thead.appendChild(header); + } + }); + + // Reorder table body cells + document.querySelectorAll('#container-rows tr').forEach(row => { + const cells = Array.from(row.children); + columnOrder.forEach(columnName => { + const cell = cells.find(c => + c.classList.contains(`table-cell-${columnName}`) || + c.dataset.content === columnName || + (columnName === 'server' && c.classList.contains('server-column')) || + (columnName === 'traefik' && c.classList.contains('traefik-column')) + ); + if (cell) { + row.appendChild(cell); + } + }); + }); + } + // Apply initial visibility + updateColumnVisibility(); + + function setupServerUI() { + serverFilterContainer.innerHTML = ''; + const servers = [...allServersData]; + + if (servers.length > 1) { + mainTable.classList.remove('table-single-server'); + + servers.sort((a, b) => { + if (a.status !== 'inactive' && b.status === 'inactive') return -1; + if (a.status === 'inactive' && b.status !== 'inactive') return 1; + if (a.order !== b.order) return a.order - b.order; + return a.name.localeCompare(b.name); + }); + + const allButton = document.createElement('button'); + allButton.textContent = 'All'; + allButton.dataset.server = 'all'; + allButton.className = 'filter-button'; + serverFilterContainer.appendChild(allButton); + + servers.forEach(server => { + const button = document.createElement('button'); + button.textContent = server.name; + button.dataset.server = server.name; + button.className = 'filter-button'; + + if (server.status === 'inactive') { + button.classList.add('inactive'); + button.disabled = true; + button.setAttribute('data-tooltip', `${server.url || 'URL unknown'} is offline`); + } else { + button.setAttribute('data-tooltip', server.url || 'URL unknown'); + } + serverFilterContainer.appendChild(button); + }); + + serverFilterContainer.querySelectorAll('.filter-button:not(:disabled)').forEach(button => { + button.addEventListener('click', () => { + currentServerFilter = button.dataset.server; + updateDisplay(); + }); + }); + } else { + mainTable.classList.add('table-single-server'); + } + + updateActiveButton(); + } + + function updateActiveButton() { + serverFilterContainer.querySelectorAll('.filter-button').forEach(button => { + button.classList.toggle('active', button.dataset.server === currentServerFilter); + }); + } + function parseAdvancedSearch(searchTerm) { + const filters = { + tags: [], + ports: [], + stacks: [], + general: [] + }; + + // Split by spaces but keep quoted strings together + const terms = searchTerm.match(/(?:[^\s"]+|"[^"]*")+/g) || []; + + terms.forEach(term => { + term = term.trim(); + if (!term) return; + + if (term.startsWith('#')) { + // Tag search + filters.tags.push(term.substring(1).toLowerCase()); + } else if (term.startsWith(':')) { + // Port search + filters.ports.push(term.substring(1)); + } else if (term.startsWith('stack:')) { + // Stack search - handle quoted values + let stackValue = term.substring(6); + if (stackValue.startsWith('"') && stackValue.endsWith('"')) { + stackValue = stackValue.slice(1, -1); + } + filters.stacks.push(stackValue.toLowerCase()); + } else { + // General search term (remove quotes if present) + if (term.startsWith('"') && term.endsWith('"')) { + term = term.slice(1, -1); + } + filters.general.push(term.toLowerCase()); + } + }); + + return filters; + } + + function updateDisplay() { + let workingData = [...allContainersData]; + + if (currentServerFilter !== "all") { + workingData = workingData.filter(c => c.server === currentServerFilter); + } + + if (filterRunningCheckbox.checked) { + workingData = workingData.filter(c => c.status === 'running' || c.status === 'healthy'); + } + + if (filterUpdatesCheckbox.checked) { + workingData = workingData.filter(c => c.update_available); + } + + const searchTerm = searchInput.value.trim(); + + if (searchTerm) { + const filters = parseAdvancedSearch(searchTerm); + + workingData = workingData.filter(container => { + // All filter conditions must be met (AND logic) + + // Check tags + if (filters.tags.length > 0) { + const hasAllTags = filters.tags.every(searchTag => + container.tags && container.tags.some(containerTag => + containerTag.toLowerCase().includes(searchTag) + ) + ); + if (!hasAllTags) return false; + } + + // Check ports + if (filters.ports.length > 0) { + const hasAllPorts = filters.ports.every(searchPort => + container.ports.some(p => + p.host_port.includes(searchPort) || + p.container_port.includes(searchPort) + ) + ); + if (!hasAllPorts) return false; + } + + // Check stacks + if (filters.stacks.length > 0) { + const hasAllStacks = filters.stacks.every(searchStack => + container.stack && container.stack.toLowerCase().includes(searchStack) + ); + if (!hasAllStacks) return false; + } + + // Check general search terms + if (filters.general.length > 0) { + const hasAllGeneral = filters.general.every(searchTerm => { + return ( + container.name.toLowerCase().includes(searchTerm) || + container.image.toLowerCase().includes(searchTerm) || + (container.stack && container.stack.toLowerCase().includes(searchTerm)) || + //(container.tags && container.tags.some(tag => tag.toLowerCase().includes(searchTerm))) || + container.ports.some(p => + p.host_port.includes(searchTerm) || + p.container_port.includes(searchTerm) + ) + ); + }); + if (!hasAllGeneral) return false; + } + + return true; + }); + } + + workingData.sort((a, b) => { + let valA = a[currentSortColumn]; + let valB = b[currentSortColumn]; + + if (currentSortColumn === "status") { + const statusOrder = { + 'starting': 1, + 'restarting': 2, + 'unhealthy': 3, + 'removing': 4, + 'created': 5, + 'paused': 6, + 'exited': 7, + 'dead': 8, + 'running': 9, + 'healthy': 10 + }; + + + valA = statusOrder[valA] || 99; + valB = statusOrder[valB] || 99; + } else if (currentSortColumn === "ports") { + const getFirstPort = (container) => { + if (container.ports.length === 0) { + return currentSortDirection === "asc" ? Number.MAX_SAFE_INTEGER : -1; + } + return parseInt(container.ports[0].host_port, 10); + }; + valA = getFirstPort(a); + valB = getFirstPort(b); + } else if (currentSortColumn === "traefik") { + const getTraefikRoutes = (container) => { + if (!container.traefik_routes || container.traefik_routes.length === 0) { + return currentSortDirection === "asc" ? "zzz_none" : ""; + } + return container.traefik_routes[0].url.toLowerCase(); + }; + valA = getTraefikRoutes(a); + valB = getTraefikRoutes(b); + } else if (typeof valA === "string" && typeof valB === "string") { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + } + + if (valA < valB) return currentSortDirection === "asc" ? -1 : 1; + if (valA > valB) return currentSortDirection === "asc" ? 1 : -1; + return 0; + }); + + + + // Check if Traefik is enabled globally and if any container has Traefik routes + const isTraefikGloballyEnabled = window.traefikEnabled !== false; // Default true if not set + const hasTraefikRoutes = isTraefikGloballyEnabled && workingData.some(c => c.traefik_routes && c.traefik_routes.length > 0); + + // Show/hide Traefik column + const traefikHeaders = document.querySelectorAll('.traefik-column'); + traefikHeaders.forEach(header => { + if (hasTraefikRoutes) { + header.classList.remove('hidden'); + } else { + header.classList.add('hidden'); + } + }); + + + const hasTags = workingData.some(c => c.tags && c.tags.length > 0); + const tagsHeaders = document.querySelectorAll('.tags-column'); + tagsHeaders.forEach(header => { + if (hasTags) { + header.classList.remove('hidden'); + } else { + header.classList.add('hidden'); + } + }); + + // Also update table cells visibility + document.querySelectorAll('.table-cell-tags').forEach(cell => { + if (hasTags) { + cell.classList.remove('hidden'); + } else { + cell.classList.add('hidden'); + } + }); + + + // Hide server column if only one server is visible after filtering + const uniqueServers = [...new Set(workingData.map(c => c.server))]; + const serverHeaders = document.querySelectorAll('.server-column'); + serverHeaders.forEach(header => { + if (uniqueServers.length <= 1) { + header.classList.add('hidden'); + } else { + header.classList.remove('hidden'); + } + }); + + // Update table class for single server styling + if (uniqueServers.length <= 1) { + mainTable.classList.add('table-single-server'); + } else { + mainTable.classList.remove('table-single-server'); + } + + filteredAndSortedContainers = workingData; + hideLoadingIndicator(); + renderTable(); + updateActiveButton(); + updateExportLink(); + updateTableColumnOrder(); + } + + function filterByStackAndServer(stack, server) { + currentServerFilter = server; + updateActiveButton(); + let stackTerm = stack.includes(" ") ? `"${stack}"` : stack; + searchInput.value = `stack:${stackTerm}`; + toggleClearButton(); + updateDisplay(); + searchInput.focus(); + } + + function toggleClearButton() { + if (searchInput.value.trim() !== '') { + clearSearchButton.classList.remove('hidden'); + } else { + clearSearchButton.classList.add('hidden'); + } + } + + function clearSearch() { + searchInput.value = ''; + clearSearchButton.classList.add('hidden'); + searchInput.focus(); + updateDisplay(); + } + + async function fetchContainerData() { + + showLoadingIndicator(); + loadFilterStates(); + try { + const response = await fetch("/data"); + if (!response.ok) throw createResponseError(response); + + const { servers = [], containers = [], traefik_enabled = true } = await response.json(); + [allServersData, allContainersData] = [servers, containers]; + window.traefikEnabled = traefik_enabled; + + isDataLoaded = true; + checkUpdatesButton.disabled = false; + + handleServerFilterReset(); + setupServerUI(); + clearSearch(); + toggleClearButton(); + filterUpdatesCheckbox.checked = false; + updateDisplay(); + + } catch (error) { + handleFetchError(error); + } finally { + hideLoadingIndicator(); + } + } + + function createResponseError(response) { + const status = response.status; + const messages = { + 401: `Authorization Error (${status}): Please log in again`, + 500: `Server Error (${status}): Please try again later`, + default: `HTTP Error: ${status} ${response.statusText}` + }; + return new Error(messages[status] || messages.default); + } + + function handleServerFilterReset() { + const shouldReset = !allServersData.some(s => s.name === currentServerFilter) || + (allServersData.find(s => s.name === currentServerFilter)?.status === 'inactive'); + if (shouldReset) { + currentServerFilter = 'all'; + } + } + + function handleFetchError(error) { + isDataLoaded = false; + checkUpdatesButton.disabled = true; + console.error("Data fetch error:", error); + const message = error.message.includes('Failed to fetch') + ? "Network Error: Could not connect to backend service" + : error.message; + displayError(message); + } + + function applyTheme(theme) { + const themeIcon = document.getElementById("theme-icon"); + if (theme === "dark") { + body.classList.add("dark-mode"); + themeIcon.innerHTML = ``; + } else { + body.classList.remove("dark-mode"); + themeIcon.innerHTML = ``; + } + localStorage.setItem("theme", theme); + } + + async function checkForUpdates() { + if (!isDataLoaded) { + return; + } + const activeServers = allServersData.filter(s => s.status === 'active'); + const serversToCheck = currentServerFilter === 'all' + ? activeServers + : activeServers.filter(s => s.name === currentServerFilter); + + if (serversToCheck.length > 1) { + try { + await showConfirmationModal( + 'Check Updates on Multiple Servers', + `You are about to check for updates on ${serversToCheck.length} servers:\n\n${serversToCheck.map(s => `• ${s.name}`).join('\n')}\n\nThis operation may take longer and will pull images from registries. Do you want to continue?`, + 'Check Updates' + ); + } catch (error) { + console.log('Multi-server update check cancelled by user'); + return; + } + } + + checkUpdatesButton.classList.add('loading'); + checkUpdatesButton.disabled = true; + + try { + const requestData = { + server_filter: currentServerFilter + }; + + const response = await fetch("/check-updates", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(requestData) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const { updates } = await response.json(); + const updatedContainers = []; + + allContainersData.forEach(container => { + const key = `${container.server}:${container.name}`; + if (updates.hasOwnProperty(key)) { + container.update_available = updates[key]; + if (updates[key]) { + updatedContainers.push(container); + } + } + }); + + updateDisplay(); + + if (updatedContainers.length > 0) { + showUpdatesModal(updatedContainers); + } else { + showNoUpdatesModal(); + } + + } catch (error) { + console.error("Update check failed:", error); + alert("Failed to check for updates. Please try again."); + } finally { + checkUpdatesButton.classList.remove('loading'); + checkUpdatesButton.disabled = false; + } + } + + checkUpdatesButton.addEventListener("click", checkForUpdates); + + + function showUpdatesModal(updatedContainers) { + const updatesList = document.getElementById("updates-list"); + updatesList.innerHTML = ""; + + updatedContainers.forEach(container => { + const li = document.createElement("li"); + li.innerHTML = `${container.name} [${container.stack}] (${container.server}) ${container.image}`; + updatesList.appendChild(li); + }); + + updatesModal.classList.remove('hidden'); + + const okHandler = () => { + updatesModal.classList.add('hidden'); + updateDisplay(); + }; + + updatesModalOkBtn.addEventListener('click', okHandler, { once: true }); + updatesModal.addEventListener('click', e => e.target === updatesModal && okHandler(), { once: true }); + } + + function showNoUpdatesModal() { + const updatesModalTitle = document.getElementById("updates-modal-title"); + const updatesList = document.getElementById("updates-list"); + + updatesModalTitle.textContent = "No Updates Available"; + updatesList.innerHTML = "
  • All containers are up to date!
  • "; + updatesModal.classList.remove('hidden'); + + const okHandler = () => { + updatesModal.classList.add('hidden'); + updatesModalTitle.textContent = "Updates Found"; + }; + + updatesModalOkBtn.addEventListener('click', okHandler, { once: true }); + updatesModal.addEventListener('click', e => e.target === updatesModal && okHandler(), { once: true }); + } + + function showConfirmationModal(title, message, confirmText = 'Confirm') { + return new Promise((resolve, reject) => { + document.getElementById('modal-title').textContent = title; + document.getElementById('modal-message').innerHTML = message.replace(/\n/g, '
    '); + modalConfirmBtn.textContent = confirmText; + modal.classList.remove('hidden'); + + const confirmHandler = () => { + modal.classList.add('hidden'); + removeListeners(); + resolve(); + }; + + const cancelHandler = () => { + modal.classList.add('hidden'); + removeListeners(); + reject(new Error('User cancelled')); + }; + + const backdropHandler = (e) => { + if (e.target === modal) { + cancelHandler(); + } + }; + + const removeListeners = () => { + modalConfirmBtn.removeEventListener('click', confirmHandler); + modalCancelBtn.removeEventListener('click', cancelHandler); + modal.removeEventListener('click', backdropHandler); + }; + + modalConfirmBtn.addEventListener('click', confirmHandler); + modalCancelBtn.addEventListener('click', cancelHandler); + modal.addEventListener('click', backdropHandler); + }); + } + + function updateExportLink() { + const exportLink = document.getElementById('export-json-link'); + if (exportLink) { + const serverParam = currentServerFilter === 'all' ? 'all' : encodeURIComponent(currentServerFilter); + exportLink.href = `/export/json?server=${serverParam}`; + } + } + + const exportLink = document.getElementById('export-json-link'); + if (exportLink) { + updateExportLink(); + } + fetchContainerData(); + applyTheme(localStorage.getItem("theme") || "dark"); + + refreshButton.addEventListener("click", fetchContainerData); + + document.getElementById("theme-switcher").addEventListener("click", () => { + applyTheme(body.classList.contains("dark-mode") ? "light" : "dark"); + }); + + filterUpdatesCheckbox.addEventListener("change", updateDisplay); + + function loadFilterStates() { + const savedRunningFilter = localStorage.getItem('filterRunningChecked'); + if (savedRunningFilter !== null) { + filterRunningCheckbox.checked = JSON.parse(savedRunningFilter); + } + } + + function saveFilterStates() { + localStorage.setItem('filterRunningChecked', JSON.stringify(filterRunningCheckbox.checked)); + } + + filterRunningCheckbox.addEventListener("change", () => { + saveFilterStates(); + updateDisplay(); + }); + + searchInput.addEventListener("input", function () { + toggleClearButton(); + updateDisplay(); + }); + + clearSearchButton.addEventListener('click', clearSearch); + + searchInput.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + clearSearch(); + } + }); + + document.querySelectorAll(".sortable-header").forEach((header) => { + header.addEventListener("click", () => { + const column = header.dataset.sortColumn; + if (column === currentSortColumn) { + currentSortDirection = currentSortDirection === "asc" ? "desc" : "asc"; + } else { + currentSortColumn = column; + currentSortDirection = "asc"; + } + document.querySelectorAll(".sortable-header").forEach(h => h.classList.remove('asc', 'desc')); + header.classList.add(currentSortDirection); + updateDisplay(); + }); + }); + + document.querySelector('.logo-title').addEventListener('click', () => { + // currentServerFilter = 'all'; + // filterRunningCheckbox.checked = false; + filterUpdatesCheckbox.checked = false; + clearSearch(); + updateDisplay(); + + }); + containerRowsBody.addEventListener('click', function (e) { + if (e.target.classList.contains('tag-badge')) { + e.preventDefault(); + const tag = e.target.dataset.tag; + const tagSearch = `#${tag}`; + + let currentSearch = searchInput.value.trim(); + + // Parse current search to check for duplicates + const filters = parseAdvancedSearch(currentSearch); + + // Check if tag already exists (case insensitive) + const tagAlreadyExists = filters.tags.some(existingTag => + existingTag.toLowerCase() === tag.toLowerCase() + ); + + if (!tagAlreadyExists) { + // Add tag to existing search + if (currentSearch) { + searchInput.value = `${currentSearch} ${tagSearch}`; + } else { + searchInput.value = tagSearch; + } + + toggleClearButton(); + updateDisplay(); + searchInput.focus(); + } + } + + if (e.target.classList.contains('stack-link')) { + e.preventDefault(); + e.stopPropagation(); + const stack = e.target.dataset.stack; + const server = e.target.dataset.server; + filterByStackAndServer(stack, server); + } + }); + updateColumnVisibility(); + initColumnDragAndDrop(); + initCustomTooltips(); +}); \ No newline at end of file diff --git a/static_old/logo.svg b/static_old/logo.svg new file mode 100644 index 0000000..f75cbbc --- /dev/null +++ b/static_old/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static_old/logo_2.svg b/static_old/logo_2.svg new file mode 100644 index 0000000..178fbd9 --- /dev/null +++ b/static_old/logo_2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/templates_old/index.html b/templates_old/index.html new file mode 100644 index 0000000..b1262ec --- /dev/null +++ b/templates_old/index.html @@ -0,0 +1,367 @@ + + + + + + + Dockpeek + + + + + +
    +
    +
    +

    dockpeek

    +
    +
    +
    + + +
    + + + + + + Logout + +
    +
    + +
    + + +
    + + + + +
    + +
    + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    + Name + + Stack + + Server + + Ports + + Traefik + + Image + + Tags + + Status +
    +
    +
    + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates_old/login.html b/templates_old/login.html new file mode 100644 index 0000000..878c995 --- /dev/null +++ b/templates_old/login.html @@ -0,0 +1,36 @@ + + + + + + + Dockpeek Login + + + + + + + + + \ No newline at end of file