diff --git a/app.py b/app.py index 31fb126..7e540b2 100644 --- a/app.py +++ b/app.py @@ -7,50 +7,128 @@ app = Flask(__name__) TRAEFIK_API_URL = "https://norman.lan/api/http/routers" REGEX_PATTERNS = [r".*\.gederico\.dynu\.net"] +DEFAULT_GROUP_PRIORITY = 100 +DEFAULT_GROUP = 'Applications' +DEFAULT_GROUP_ICON = 'fas fa-box' +DEFAULT_TITLE = 'Traefik Routers' client = docker.from_env() -containers = client.containers.list(all=True) # Get all containers -def get_routers(): +def get_routers(containers): response = requests.get(TRAEFIK_API_URL, verify=False) # Ignore SSL certificate verification if response.status_code == 200: routers = response.json() - filtered_routers = filter_routers(routers) + filtered_routers = filter_routers(routers, containers) return filtered_routers return [] -def filter_routers(routers): +def filter_routers(routers, containers): filtered_routers = [] for router in routers: for pattern in REGEX_PATTERNS: if re.match(pattern, router['rule'].split('`')[1]): - router['description'] = get_router_description(router['name']) - filtered_routers.append(router) + if not is_router_hidden(router['name'], containers): + router['description'] = get_router_description(router['name'], containers) + router['display_name'] = get_router_display_name(router['name'], containers) + router['group'] = get_router_group(router['name'], containers) + filtered_routers.append(router) break return filtered_routers -def get_router_description(router_name): - try: - service_name = router_name.split('@')[0] - for container in containers: - labels = container.attrs.get('Config', {}).get('Labels', {}) - description = labels.get('traefik-frontend.http.routers.' + service_name + '.description') - if description: - return description - except Exception as e: - print(f"Error fetching description for {router_name}: {e}") +def is_router_hidden(router_name, containers): + service_name = router_name.split('@')[0] + for container in containers: + labels = container.attrs.get('Config', {}).get('Labels', {}) + hidden_label = f'traefik-frontend.http.routers.{service_name}.hidden' + if labels.get(hidden_label) == 'true': + return True + return False + +def get_router_description(router_name, containers): + service_name = router_name.split('@')[0] + for container in containers: + labels = container.attrs.get('Config', {}).get('Labels', {}) + description_label = f'traefik-frontend.http.routers.{service_name}.description' + if description_label in labels: + return labels[description_label] return 'No description available' -def truncate_and_uppercase_name(name): - return name.split('@')[0].upper() +def get_router_display_name(router_name, containers): + service_name = router_name.split('@')[0] + for container in containers: + labels = container.attrs.get('Config', {}).get('Labels', {}) + display_name_label = f'traefik-frontend.http.routers.{service_name}.router_name' + if display_name_label in labels: + return labels[display_name_label].upper() + return service_name.upper() + +def get_router_group(router_name, containers): + service_name = router_name.split('@')[0] + for container in containers: + labels = container.attrs.get('Config', {}).get('Labels', {}) + group_label = f'traefik-frontend.http.routers.{service_name}.group' + if group_label in labels: + return labels[group_label] + return DEFAULT_GROUP + +def get_groups(containers): + groups = { + DEFAULT_GROUP: { + 'priority': DEFAULT_GROUP_PRIORITY, + 'collapsed': False, + 'routers': [], + 'icon': DEFAULT_GROUP_ICON + } + } + + for container in containers: + labels = container.attrs.get('Config', {}).get('Labels', {}) + for label, value in labels.items(): + if label.startswith('traefik-frontend.groups.'): + group_name = label.split('.')[2] + priority_label = f'traefik-frontend.groups.{group_name}.priority' + collapsed_label = f'traefik-frontend.groups.{group_name}.collapsed' + icon_label = f'traefik-frontend.groups.{group_name}.icon' + + if group_name not in groups: + groups[group_name] = { + 'priority': DEFAULT_GROUP_PRIORITY, + 'collapsed': False, + 'routers': [], + 'icon': DEFAULT_GROUP_ICON + } + + if priority_label in labels: + groups[group_name]['priority'] = int(labels[priority_label]) + if collapsed_label in labels: + groups[group_name]['collapsed'] = labels[collapsed_label].lower() == 'true' + if icon_label in labels: + groups[group_name]['icon'] = labels[icon_label] + + return groups + +def get_title(containers): + for container in containers: + labels = container.attrs.get('Config', {}).get('Labels', {}) + title_label = 'traefik-frontend.title' + if title_label in labels: + return labels[title_label] + return DEFAULT_TITLE @app.route('/') def index(): - routers = get_routers() + containers = client.containers.list(all=True) # Get all containers once + title = get_title(containers) + routers = get_routers(containers) + groups = get_groups(containers) for router in routers: - router['truncated_name'] = truncate_and_uppercase_name(router['name']) - return render_template('index.html', routers=routers) + group_name = router['group'] + if group_name not in groups: + group_name = DEFAULT_GROUP + groups[group_name]['routers'].append(router) + sorted_groups = {k: v for k, v in sorted(groups.items(), key=lambda item: item[1]['priority']) if v['routers']} + return render_template('index.html', title=title, groups=sorted_groups) if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) + app.run(debug=True, host='0.0.0.0') diff --git a/docker-compose.yml b/docker-compose.yml index aaf1b1b..eca4af2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: traefik-frontend: container_name: traefik-frontend @@ -20,6 +18,27 @@ services: - "traefik.http.routers.home.middlewares=chain-authelia@file" - "traefik.http.services.home.loadbalancer.server.port=5000" - "traefik.http.services.home.loadbalancer.server.scheme=http" + - "traefik-frontend.http.routers.home.hidden=true" + - "traefik-frontend.http.routers.eve.group=Networking" + - "traefik-frontend.http.routers.clab.group=Networking" + - "traefik-frontend.http.routers.tm.group=Media" + - "traefik-frontend.http.routers.tm.router_name=Transmission" + - "traefik-frontend.http.routers.flexget.group=Automation" + - "traefik-frontend.http.routers.connpy.group=Development" + - "traefik-frontend.http.routers.nginx.hidden=true" + - "traefik-frontend.groups.Management.priority=1" + - "traefik-frontend.groups.Management.icon=fas fa-tools" + - "traefik-frontend.groups.Networking.priority=2" + - "traefik-frontend.groups.Networking.icon=fas fa-network-wired" + - "traefik-frontend.groups.Media.priority=3" + - "traefik-frontend.groups.Media.icon=fas fa-film" + - "traefik-frontend.groups.Development.priority=4" + - "traefik-frontend.groups.Development.icon=fas fa-laptop-code" + - "traefik-frontend.groups.Automation.priority=5" + - "traefik-frontend.groups.Automation.icon=fas fa-robot" + - "traefik-frontend.groups.Security.priority=6" + - "traefik-frontend.groups.Security.icon=fas fa-lock" + - "traefik-frontend.title=GEDERICO'S HOME" networks: web: diff --git a/static/css/colors.css b/static/css/colors.css new file mode 100644 index 0000000..8ac932e --- /dev/null +++ b/static/css/colors.css @@ -0,0 +1,115 @@ +:root { + --light-background: #f9f9f9; + --dark-background: #121212; + --light-title-text: #e0e0e0; + --light-icons: #333; + --dark-title-text: #e0e0e0; + --dark-icons: #e0e0e0; + --light-header: #3f51b5; + --light-group-text: #3f51b5; + --light-router-title: #3f51b5; + --dark-header: #1e1e1e; + --light-border: #3f51b5; + --dark-border: #bb86fc; + --dark-group-text: #bb86fc; + --dark-router-title: #bb86fc; + --light-icon-bg: rgba(240, 240, 240, 0.2); + --dark-icon-bg: rgba(224, 224, 224, 0.2); + --light-icon-hover-bg: rgba(240, 240, 240, 0.4); + --dark-icon-hover-bg: rgba(224, 224, 224, 0.4); + --light-router-bg: white; + --dark-router-bg: #1e1e1e; + --light-router-hover-shadow: rgba(0, 0, 0, 0.2); + --light-router-text: #666; + --dark-router-text: #e0e0e0; + --dark-router-hover-shadow: rgba(255, 255, 255, 0.2); +} + +body { + background-color: var(--light-background); + color: var(--light-title-text); +} + +.dark-mode { + background-color: var(--dark-background); + color: var(--dark-title-text); +} + +.header { + background-color: var(--light-header); +} + +.dark-mode .header { + background-color: var(--dark-header); +} + +.icon-button { + background-color: var(--light-icon-bg); + border: 1px solid var(--light-border); + color: var(--light-icons); +} + +.icon-button:hover { + background-color: var(--light-icon-hover-bg); +} + +.dark-mode .icon-button { + background-color: var(--dark-icon-bg); + border: 1px solid var(--dark-border); + color: var(--dark-icons); +} + +.dark-mode .icon-button:hover { + background-color: var(--dark-icon-hover-bg); +} + +.group h2 { + color: var(--light-group-text); +} + +.dark-mode .group h2 { + color: var(--dark-group-text); +} + +.router { + background-color: var(--light-router-bg); + color: var(--light-router-text); +} + +.router:hover { + box-shadow: 0 4px 8px var(--light-router-hover-shadow); +} + +.router h2 { + color: var(--light-router-title); +} + +.dark-mode .router h2 { + color: var(--dark-router-title); +} + +.dark-mode .router { + background-color: var(--dark-router-bg); + color: var(--dark-router-text); +} + +.dark-mode .router:hover { + box-shadow: 0 4px 8px var(--dark-router-hover-shadow); +} + +.group-title .fa { + color: var(--light-group-text); +} + +.dark-mode .group-title .fa { + color: var(--dark-group-text); +} + +.settings-menu .fa { + color: var(--light-icons); +} + +.dark-mode .settings-menu .fa { + color: var(--dark-icons); +} + diff --git a/static/css/components.css b/static/css/components.css new file mode 100644 index 0000000..1e92a84 --- /dev/null +++ b/static/css/components.css @@ -0,0 +1,77 @@ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; +} + +.header h1 { + margin: 0; + flex-grow: 1; + text-align: center; +} + +.settings { + position: relative; +} + +.icon-button { + font-size: 24px; + cursor: pointer; + margin: 5px; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s, border-color 0.3s; +} + +.settings-menu { + display: none; + position: absolute; + top: 50px; + right: 0; + flex-direction: column; +} + +.settings-menu.visible { + display: flex; +} + +.group { + margin-bottom: 40px; +} + +.group-title { + display: inline-block; + cursor: pointer; +} + +.group-icon { + margin-right: 8px; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .router { + flex: 1 1 calc(50% - 20px); + max-width: calc(50% - 20px); + } +} + +@media (max-width: 768px) { + .router { + flex: 1 1 calc(50% - 20px); + max-width: calc(50% - 20px); + } +} + +@media (max-width: 480px) { + .router { + flex: 1 1 100%; + max-width: 100%; + } +} + diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..b383143 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,37 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + transition: background-color 0.3s, color 0.3s; +} + +.content { + padding: 20px; +} + +.group { + margin-bottom: 40px; +} + +.group.collapsed .router-container { + display: none; +} + +.router-container { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +.router { + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 20px; + margin: 10px; + flex: 1 1 calc(33.333% - 20px); + max-width: calc(33.333% - 20px); + text-align: center; + cursor: pointer; + transition: box-shadow 0.3s ease; + box-sizing: border-box; +} + diff --git a/static/js/scripts.js b/static/js/scripts.js new file mode 100644 index 0000000..831aaab --- /dev/null +++ b/static/js/scripts.js @@ -0,0 +1,46 @@ +document.addEventListener("DOMContentLoaded", function() { + if (localStorage.getItem("dark-mode") === "true") { + document.body.classList.add("dark-mode"); + document.getElementById('theme-icon').classList.remove('fa-moon'); + document.getElementById('theme-icon').classList.add('fa-sun'); + } else { + document.getElementById('theme-icon').classList.remove('fa-sun'); + document.getElementById('theme-icon').classList.add('fa-moon'); + } + + document.addEventListener("click", function(event) { + const settingsMenu = document.getElementById('settings-menu'); + if (!settingsMenu.contains(event.target) && !event.target.closest('.icon-button')) { + settingsMenu.classList.remove('visible'); + } + }); +}); + +function toggleDarkMode() { + document.body.classList.toggle("dark-mode"); + const themeIcon = document.getElementById('theme-icon'); + if (document.body.classList.contains("dark-mode")) { + localStorage.setItem("dark-mode", "true"); + themeIcon.classList.remove('fa-moon'); + themeIcon.classList.add('fa-sun'); + } else { + localStorage.setItem("dark-mode", "false"); + themeIcon.classList.remove('fa-sun'); + themeIcon.classList.add('fa-moon'); + } +} + +function toggleGroup(group) { + const groupElement = document.getElementById(group); + groupElement.classList.toggle("collapsed"); +} + +function toggleSettings() { + const settingsMenu = document.getElementById('settings-menu'); + settingsMenu.classList.toggle('visible'); +} + +function refreshData() { + location.reload(); +} + diff --git a/static/styles.css b/static/styles.css deleted file mode 100644 index 2082968..0000000 --- a/static/styles.css +++ /dev/null @@ -1,136 +0,0 @@ -body { - font-family: Arial, sans-serif; - margin: 0; - background-color: #f9f9f9; - color: #333; - transition: background-color 0.3s, color 0.3s; -} - -.header { - background-color: #3f51b5; - color: white; - padding: 20px; - text-align: center; -} - -.header button { - background: none; - border: 2px solid white; - color: white; - padding: 10px 20px; - cursor: pointer; - margin-top: 10px; - border-radius: 5px; -} - -.header button:hover { - background-color: white; - color: #3f51b5; -} - -.content { - padding: 20px; -} - -.applications { - max-width: 1200px; - margin: 0 auto; -} - -.applications h2 { - color: #3f51b5; -} - -.router-container { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} - -.router { - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 20px; - margin: 10px; - flex: 1 1 calc(33.333% - 20px); - max-width: calc(33.333% - 20px); - text-align: center; - cursor: pointer; - transition: box-shadow 0.3s ease; - box-sizing: border-box; -} - -.router:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - -.router h2 { - margin: 0; - color: #3f51b5; - text-transform: uppercase; -} - -.router p { - margin: 10px 0 0; - color: #666; -} - -/* Dark Mode */ -.dark-mode { - background-color: #121212; - color: #e0e0e0; -} - -.dark-mode .header { - background-color: #1e1e1e; -} - -.dark-mode .header button { - border-color: #e0e0e0; -} - -.dark-mode .header button:hover { - background-color: #e0e0e0; - color: #1e1e1e; -} - -.dark-mode .applications h2 { - color: #bb86fc; -} - -.dark-mode .router { - background-color: #1e1e1e; - color: #e0e0e0; -} - -.dark-mode .router h2 { - color: #bb86fc; -} - -.dark-mode .router:hover { - box-shadow: 0 4px 8px rgba(255, 255, 255, 0.2); -} - -/* Responsive Design */ -@media (max-width: 1024px) { - .router { - flex: 1 1 calc(50% - 20px); - max-width: calc(50% - 20px); - } -} - -@media (max-width: 768px) { - .router { - flex: 1 1 calc(50% - 20px); - max-width: calc(50% - 20px); - } -} - -@media (max-width: 480px) { - .router { - flex: 1 1 100%; - max-width: 100%; - } -} - diff --git a/templates/index.html b/templates/index.html index c812017..02a8bcd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,39 +3,39 @@ - Traefik Routers - - + {{ title }} + + + + +
-

Traefik Routers

- -
-
-
-

Applications

-
- {% for router in routers %} -
-

{{ router['truncated_name'] }}

-

{{ router['description'] }}

-
- {% endfor %} +

{{ title }}

+
+ +
+ +
+
+ {% for group, data in groups.items() %} +
+

{{ group }}

+
+ {% for router in data.routers %} +
+

{{ router['display_name'] }}

+

{{ router['description'] }}

+
+ {% endfor %} +
+
+ {% endfor %} +