This commit is contained in:
2025-09-06 06:23:56 +02:00
parent 00029680d0
commit a29252c716
7 changed files with 1195 additions and 0 deletions

753
main.py Normal file
View File

@@ -0,0 +1,753 @@
#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk
import pystray
from PIL import Image, ImageDraw
import threading
import sys
import queue
from models import Customer, Location, Host
from data_loader import load_customers
class VPNManagerWindow:
def __init__(self):
self.window = tk.Tk()
self.window.title("VPN Manager")
self.window.geometry("1200x750")
self.window.minsize(1000, 600)
self.window.protocol("WM_DELETE_WINDOW", self.hide_window)
# Command queue for thread communication
self.command_queue = queue.Queue()
# Configure modern theme colors
self.bg_color = "#1a1d29" # Deep dark blue
self.fg_color = "#e8eaf6" # Soft white
self.entry_bg = "#2d3142" # Dark gray-blue
self.button_bg = "#252836" # Card background
self.hover_bg = "#3a3f5c" # Hover state
self.accent_color = "#5e72e4" # Primary accent (blue)
self.success_color = "#2dce89" # Green for connected
self.danger_color = "#f5365c" # Red for disconnected
self.warning_color = "#fb6340" # Orange for warnings
self.window.configure(bg=self.bg_color)
# Load customer data using dataclasses
self.customers = load_customers()
self.filtered_customers = self.customers.copy()
self.icon = None
self.setup_ui()
self.setup_tray()
self.hide_window()
def setup_ui(self):
# Header with gradient effect (simulated with frame)
header_frame = tk.Frame(self.window, bg=self.bg_color)
header_frame.pack(fill=tk.X, padx=20, pady=(20, 10))
# Title with icon
title_frame = tk.Frame(header_frame, bg=self.bg_color)
title_frame.pack(side=tk.TOP)
# Shield icon next to title
icon_label = tk.Label(
title_frame,
text="🛡️",
font=("Segoe UI Emoji", 24),
bg=self.bg_color,
fg=self.accent_color
)
icon_label.pack(side=tk.LEFT, padx=(0, 10))
title_label = tk.Label(
title_frame,
text="VPN Connection Manager",
font=("Segoe UI", 20, "bold"),
bg=self.bg_color,
fg=self.fg_color
)
title_label.pack(side=tk.LEFT)
# Search bar with modern styling
search_container = tk.Frame(header_frame, bg=self.bg_color)
search_container.pack(fill=tk.X, pady=(20, 10))
search_frame = tk.Frame(search_container, bg=self.entry_bg,
highlightbackground=self.accent_color, highlightthickness=1)
search_frame.pack(fill=tk.X)
search_label = tk.Label(
search_frame,
text="🔍",
font=("Segoe UI Emoji", 14),
bg=self.entry_bg,
fg=self.accent_color
)
search_label.pack(side=tk.LEFT, padx=(10, 5))
self.search_var = tk.StringVar()
self.search_var.trace("w", self.filter_customers)
search_entry = tk.Entry(
search_frame,
textvariable=self.search_var,
font=("Segoe UI", 12),
bg=self.entry_bg,
fg=self.fg_color,
insertbackground=self.accent_color,
relief=tk.FLAT,
bd=0
)
search_entry.pack(side=tk.LEFT, fill=tk.X,
expand=True, padx=(0, 10), pady=10)
# Main container for two columns
self.columns_container = tk.Frame(self.window, bg=self.bg_color)
self.columns_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Left column - Active customers
left_column = tk.Frame(self.columns_container, bg=self.bg_color)
left_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# Active customers header
active_header = tk.Frame(left_column, bg=self.bg_color)
active_header.pack(fill=tk.X, pady=(0, 10))
active_title = tk.Label(
active_header,
text="✅ Active Customers",
font=("Segoe UI", 14, "bold"),
bg=self.bg_color,
fg=self.success_color
)
active_title.pack(side=tk.LEFT)
# Active customers scrollable area
active_canvas = tk.Canvas(
left_column, bg=self.bg_color, highlightthickness=0)
active_scrollbar = ttk.Scrollbar(
left_column, orient="vertical", command=active_canvas.yview)
self.active_frame = tk.Frame(active_canvas, bg=self.bg_color)
self.active_frame.bind(
"<Configure>",
lambda e: active_canvas.configure(
scrollregion=active_canvas.bbox("all"))
)
active_canvas.create_window(
(0, 0), window=self.active_frame, anchor="nw")
active_canvas.configure(yscrollcommand=active_scrollbar.set)
active_canvas.pack(side="left", fill="both", expand=True)
active_scrollbar.pack(side="right", fill="y")
# Separator
separator = tk.Frame(self.columns_container, bg="#3a3f5c", width=2)
separator.pack(side=tk.LEFT, fill=tk.Y, padx=5)
# Right column - Inactive customers
right_column = tk.Frame(self.columns_container, bg=self.bg_color)
right_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0))
# Inactive customers header
inactive_header = tk.Frame(right_column, bg=self.bg_color)
inactive_header.pack(fill=tk.X, pady=(0, 10))
inactive_title = tk.Label(
inactive_header,
text="💤 Inactive Customers",
font=("Segoe UI", 14, "bold"),
bg=self.bg_color,
fg="#8892b0"
)
inactive_title.pack(side=tk.LEFT)
# Inactive customers scrollable area
inactive_canvas = tk.Canvas(
right_column, bg=self.bg_color, highlightthickness=0)
inactive_scrollbar = ttk.Scrollbar(
right_column, orient="vertical", command=inactive_canvas.yview)
self.inactive_frame = tk.Frame(inactive_canvas, bg=self.bg_color)
self.inactive_frame.bind(
"<Configure>",
lambda e: inactive_canvas.configure(
scrollregion=inactive_canvas.bbox("all"))
)
inactive_canvas.create_window(
(0, 0), window=self.inactive_frame, anchor="nw")
inactive_canvas.configure(yscrollcommand=inactive_scrollbar.set)
inactive_canvas.pack(side="left", fill="both", expand=True)
inactive_scrollbar.pack(side="right", fill="y")
self.active_customer_frames = []
self.inactive_customer_frames = []
self.render_customers()
def setup_tray(self):
# Create tray icon in a separate thread
def run_tray():
image = self.create_tray_icon()
menu = pystray.Menu(
pystray.MenuItem("Open VPN Manager",
self.show_window_from_tray, default=True),
pystray.MenuItem("Quit", self.quit_app)
)
# Set both menu and default click action
self.icon = pystray.Icon("VPNTray", image, menu=menu)
# Add left-click handler
def on_clicked(icon, item):
print("Tray icon clicked!")
self.show_window_from_tray()
self.icon.default_menu_item = menu.items[0]
print("Starting system tray icon...")
self.icon.run()
tray_thread = threading.Thread(target=run_tray, daemon=True)
tray_thread.start()
def render_customers(self):
# Clear existing customer frames
for frame in self.active_customer_frames:
frame.destroy()
self.active_customer_frames.clear()
for frame in self.inactive_customer_frames:
frame.destroy()
self.inactive_customer_frames.clear()
# Separate customers with active and inactive locations
customers_with_active = []
customers_with_inactive = []
for customer in self.filtered_customers:
active_locations = customer.get_active_locations()
inactive_locations = customer.get_inactive_locations()
if active_locations:
# Customer has at least one active location - show only active locations
customer_data = Customer(
name=customer.name,
locations=active_locations
)
customers_with_active.append(customer_data)
if inactive_locations:
# Customer has at least one inactive location - show only inactive locations
customer_data = Customer(
name=customer.name,
locations=inactive_locations
)
customers_with_inactive.append(customer_data)
# Render customers with active locations on the left
for customer in customers_with_active:
self.create_customer_with_active_locations(customer)
# Render customers with inactive locations on the right
for customer in customers_with_inactive:
self.create_customer_without_active_locations(customer)
def create_customer_with_active_locations(self, customer):
# Customer container card
customer_frame = tk.Frame(
self.active_frame,
bg=self.button_bg,
relief=tk.FLAT,
highlightbackground="#3a3f5c",
highlightthickness=1
)
customer_frame.pack(fill=tk.X, pady=8, padx=5)
self.active_customer_frames.append(customer_frame)
# Customer header
customer_header = tk.Frame(customer_frame, bg=self.button_bg)
customer_header.pack(fill=tk.X, padx=15, pady=(10, 5))
customer_label = tk.Label(
customer_header,
text=f"🏢 {customer.name}",
font=("Segoe UI", 14, "bold"),
bg=self.button_bg,
fg=self.accent_color
)
customer_label.pack(anchor="w")
# Render each active location
for location in customer.locations:
self.create_active_location_card(
location, customer_frame, customer.name)
def create_active_location_card(self, location, parent_frame, customer_name):
# Location card within customer container
card_frame = tk.Frame(
parent_frame,
bg="#2a2e3f",
relief=tk.FLAT,
highlightbackground="#3a3f5c",
highlightthickness=1
)
card_frame.pack(fill=tk.X, pady=5, padx=(20, 10))
# Location header
header = tk.Frame(card_frame, bg="#2a2e3f")
header.pack(fill=tk.X, padx=10, pady=8)
# Location name and VPN type
info_frame = tk.Frame(header, bg="#2a2e3f")
info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Location name
location_label = tk.Label(
info_frame,
text=f"📍 {location.name}",
font=("Segoe UI", 12, "bold"),
bg="#2a2e3f",
fg=self.fg_color,
anchor="w"
)
location_label.pack(anchor="w")
# VPN type with icon
vpn_icons = {
"OpenVPN": "🔒",
"WireGuard": "",
"IPSec": "🛡️"
}
vpn_icon = vpn_icons.get(location.vpn_type, "🔑")
type_label = tk.Label(
info_frame,
text=f"{vpn_icon} {location.vpn_type} VPN",
font=("Segoe UI", 10),
bg="#2a2e3f",
fg="#8892b0",
anchor="w"
)
type_label.pack(anchor="w")
# Connection controls
controls_frame = tk.Frame(header, bg="#2a2e3f")
controls_frame.pack(side=tk.RIGHT)
# Status indicator with modern colors
status_color = self.success_color if location.connected else self.danger_color
status_text = "● Connected" if location.connected else "○ Disconnected"
status_frame = tk.Frame(controls_frame, bg="#2a2e3f")
status_frame.pack(side=tk.LEFT, padx=(0, 10))
status_label = tk.Label(
status_frame,
text=status_text,
font=("Segoe UI", 10, "bold"),
bg="#2a2e3f",
fg=status_color
)
status_label.pack()
# Connect/Disconnect button with modern styling
btn_text = "Disconnect" if location.connected else "Connect"
btn_bg = self.danger_color if location.connected else self.accent_color
connect_btn = tk.Button(
controls_frame,
text=btn_text,
font=("Segoe UI", 10, "bold"),
bg=btn_bg,
fg="white",
activebackground=self.hover_bg,
activeforeground="white",
relief=tk.FLAT,
padx=15,
pady=5,
cursor="hand2",
command=lambda l=location: self.toggle_connection(l)
)
connect_btn.pack(side=tk.LEFT, padx=3)
# Route button with modern styling
route_btn = tk.Button(
controls_frame,
text="Routes",
font=("Segoe UI", 10),
bg=self.hover_bg,
fg=self.fg_color,
activebackground=self.accent_color,
activeforeground="white",
relief=tk.FLAT,
padx=15,
pady=5,
cursor="hand2",
command=lambda l=location: self.set_route(l)
)
route_btn.pack(side=tk.LEFT, padx=3)
# Deactivate button
deactivate_btn = tk.Button(
controls_frame,
text="Deactivate",
font=("Segoe UI", 10),
bg=self.warning_color,
fg="white",
activebackground=self.hover_bg,
activeforeground="white",
relief=tk.FLAT,
padx=15,
pady=5,
cursor="hand2",
command=lambda l=location: self.deactivate_location(
l, customer_name)
)
deactivate_btn.pack(side=tk.LEFT, padx=3)
# Hosts/Services section
if location.hosts:
hosts_frame = tk.Frame(card_frame, bg="#2a2e3f")
hosts_frame.pack(fill=tk.X, padx=20, pady=(0, 10))
# Services header with separator
separator = tk.Frame(hosts_frame, bg="#3a3f5c", height=1)
separator.pack(fill=tk.X, pady=(0, 8))
hosts_label = tk.Label(
hosts_frame,
text="💼 Available Services",
font=("Segoe UI", 10, "bold"),
bg="#2a2e3f",
fg=self.accent_color,
anchor="w"
)
hosts_label.pack(anchor="w", pady=(0, 8))
for host in location.hosts:
# Modern service item with rounded appearance
host_item = tk.Frame(
hosts_frame, bg="#1a1d29", highlightbackground="#3a3f5c", highlightthickness=1)
host_item.pack(fill=tk.X, pady=3, padx=2)
# Host type icon with improved icons
type_icon = {
"SSH": "💻",
"Web": "🌐",
"SMB": "📂",
"PostgreSQL": "🗃️",
"Redis": "🗂️"
}.get(host.type, "📡")
# Left side with icon and text
info_frame = tk.Frame(host_item, bg="#1a1d29")
info_frame.pack(side=tk.LEFT, fill=tk.X,
expand=True, padx=10, pady=6)
icon_label = tk.Label(
info_frame,
text=type_icon,
font=("Segoe UI Emoji", 12),
bg="#1a1d29"
)
icon_label.pack(side=tk.LEFT, padx=(0, 8))
# Service details
details_frame = tk.Frame(info_frame, bg="#1a1d29")
details_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
name_label = tk.Label(
details_frame,
text=host.name,
font=("Segoe UI", 9, "bold"),
bg="#1a1d29",
fg=self.fg_color,
anchor="w"
)
name_label.pack(anchor="w")
addr_label = tk.Label(
details_frame,
text=host.address,
font=("Consolas", 8),
bg="#1a1d29",
fg="#8892b0",
anchor="w"
)
addr_label.pack(anchor="w")
# Quick access button with better styling
if host.type in ["SSH", "Web"]:
access_btn = tk.Button(
host_item,
text="Launch",
font=("Segoe UI", 9, "bold"),
bg=self.accent_color,
fg="white",
activebackground=self.hover_bg,
activeforeground="white",
relief=tk.FLAT,
padx=12,
pady=4,
cursor="hand2",
command=lambda h=host: self.open_service(h)
)
access_btn.pack(side=tk.RIGHT, padx=8, pady=4)
def create_customer_without_active_locations(self, customer):
# Customer container card
customer_frame = tk.Frame(
self.inactive_frame,
bg=self.button_bg,
relief=tk.FLAT,
highlightbackground="#3a3f5c",
highlightthickness=1
)
customer_frame.pack(fill=tk.X, pady=8, padx=5)
self.inactive_customer_frames.append(customer_frame)
# Customer header
customer_header = tk.Frame(customer_frame, bg=self.button_bg)
customer_header.pack(fill=tk.X, padx=15, pady=(10, 5))
customer_label = tk.Label(
customer_header,
text=f"🏢 {customer.name}",
font=("Segoe UI", 14, "bold"),
bg=self.button_bg,
fg="#8892b0"
)
customer_label.pack(anchor="w")
# Render all locations for this customer
for location in customer.locations:
self.create_inactive_location_card(
location, customer_frame, customer.name)
def create_inactive_location_card(self, location, parent_frame, customer_name):
# Location card within customer container
card_frame = tk.Frame(
parent_frame,
bg="#2a2e3f",
relief=tk.FLAT,
highlightbackground="#3a3f5c",
highlightthickness=1
)
card_frame.pack(fill=tk.X, pady=5, padx=(20, 10))
# Location info container
info_container = tk.Frame(card_frame, bg="#2a2e3f")
info_container.pack(fill=tk.X, padx=12, pady=10)
# Location and VPN type
info_frame = tk.Frame(info_container, bg="#2a2e3f")
info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Location name
location_label = tk.Label(
info_frame,
text=f"📍 {location.name}",
font=("Segoe UI", 12, "bold"),
bg="#2a2e3f",
fg=self.fg_color,
anchor="w"
)
location_label.pack(anchor="w")
# VPN type with icon
vpn_icons = {
"OpenVPN": "🔒",
"WireGuard": "",
"IPSec": "🛡️"
}
vpn_icon = vpn_icons.get(location.vpn_type, "🔑")
type_label = tk.Label(
info_frame,
text=f"{vpn_icon} {location.vpn_type} VPN",
font=("Segoe UI", 10),
bg="#2a2e3f",
fg="#8892b0",
anchor="w"
)
type_label.pack(anchor="w", pady=(2, 0))
# Service count summary
service_count = len(location.hosts)
services_label = tk.Label(
info_frame,
text=f"📊 {service_count} services available",
font=("Segoe UI", 9),
bg="#2a2e3f",
fg="#6c757d",
anchor="w"
)
services_label.pack(anchor="w", pady=(4, 0))
# Set Active button
activate_btn = tk.Button(
info_container,
text="Set Active",
font=("Segoe UI", 10, "bold"),
bg=self.accent_color,
fg="white",
activebackground=self.hover_bg,
activeforeground="white",
relief=tk.FLAT,
padx=20,
pady=8,
cursor="hand2",
command=lambda l=location: self.set_location_active(
l, customer_name)
)
activate_btn.pack(side=tk.RIGHT)
def set_location_active(self, location, customer_name):
# Set this location as active
# Find the location in the original data structure and update it
for customer in self.customers:
if customer.name == customer_name:
target_location = customer.get_location_by_name(location.name)
if target_location:
target_location.active = True
print(
f"Mock: Setting {customer.name} - {target_location.name} as active")
break
self.render_customers()
def deactivate_location(self, location, customer_name):
# Deactivate this location
for customer in self.customers:
if customer.name == customer_name:
target_location = customer.get_location_by_name(location.name)
if target_location:
target_location.active = False
print(
f"Mock: Deactivating {customer.name} - {target_location.name}")
break
self.render_customers()
def filter_customers(self, *args):
search_term = self.search_var.get().lower()
if search_term:
self.filtered_customers = []
for customer in self.customers:
# Check if search term matches customer name
if search_term in customer.name.lower():
self.filtered_customers.append(customer)
else:
# Check if search term matches any location or host
matching_locations = []
for location in customer.locations:
if (search_term in location.name.lower() or
search_term in location.vpn_type.lower() or
any(search_term in h.name.lower() or
search_term in h.address.lower()
for h in location.hosts)):
matching_locations.append(location)
if matching_locations:
# Create a filtered customer with only matching locations
filtered_customer = Customer(
name=customer.name,
locations=matching_locations
)
self.filtered_customers.append(filtered_customer)
else:
self.filtered_customers = self.customers.copy()
self.render_customers()
def toggle_connection(self, location):
# Mock toggle connection for location
location.connected = not location.connected
status = "connected to" if location.connected else "disconnected from"
print(f"Mock: {status} - {location.name} via {location.vpn_type}")
self.render_customers()
def set_route(self, location):
print(f"Mock: Setting route for {location.name}")
def open_service(self, host):
print(
f"Mock: Opening {host.type} service: {host.name} at {host.address}")
def show_window_from_tray(self, icon=None, item=None):
# Put command in queue to be processed by main thread
print("Adding show_window to queue...")
self.command_queue.put("show_window")
def show_window(self):
print("Showing window...")
self.window.deiconify()
self.window.lift()
self.window.focus_force()
print("Window should be visible now")
def hide_window(self):
self.window.withdraw()
def quit_app(self, icon=None, item=None):
if self.icon:
self.icon.stop()
self.window.quit()
sys.exit(0)
def create_tray_icon(self):
# Create a simple VPN icon
width = 64
height = 64
image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(image)
# Draw a shield shape for VPN icon
shield_points = [
(32, 10), # Top center
(50, 20), # Top right
(50, 35), # Right side
(32, 54), # Bottom point
(14, 35), # Left side
(14, 20), # Top left
(32, 10) # Close shape
]
draw.polygon(shield_points, fill=(
66, 135, 245), outline=(255, 255, 255))
# Draw a lock symbol
draw.ellipse([25, 23, 39, 32], fill=(255, 255, 255))
draw.rectangle([27, 28, 37, 38], fill=(255, 255, 255))
return image
def process_queue(self):
# Process commands from the queue
try:
while True:
command = self.command_queue.get_nowait()
print(f"Processing command: {command}")
if command == "show_window":
self.show_window()
except queue.Empty:
pass
# Schedule next check
self.window.after(100, self.process_queue)
def run(self):
# Start processing queue
self.process_queue()
# Start the tkinter main loop in the main thread
self.window.mainloop()
def main():
app = VPNManagerWindow()
app.run()
if __name__ == "__main__":
main()