Files
VPNTray/main.py
2025-09-06 06:49:20 +02:00

660 lines
23 KiB
Python

#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('AppIndicator3', '0.1')
from gi.repository import Gtk, Gdk, GLib, Gio, AppIndicator3
import threading
import sys
from models import Customer, Location, Host
from data_loader import load_customers
class VPNManagerWindow:
def __init__(self):
self.customers = load_customers()
self.filtered_customers = self.customers.copy()
# Create main window
self.window = Gtk.Window()
self.window.set_title("VPN Manager")
self.window.set_default_size(1200, 750)
self.window.connect("delete-event", self.hide_window)
# Set up CSS for dark theme
self.setup_css()
# Create UI
self.setup_ui()
self.setup_system_tray()
# Start hidden
self.window.hide()
def setup_css(self):
css_provider = Gtk.CssProvider()
css = """
window {
background-color: #1a1d29;
color: #e8eaf6;
}
.header {
background: linear-gradient(135deg, #1a1d29 0%, #252836 100%);
padding: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
color: #e8eaf6;
}
.search-entry {
background-color: #2d3142;
color: #e8eaf6;
border: 1px solid #5e72e4;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
}
.customer-card {
background-color: #252836;
border: 1px solid #3a3f5c;
border-radius: 8px;
margin: 8px 5px;
padding: 15px;
}
.location-card {
background-color: #2a2e3f;
border: 1px solid #3a3f5c;
border-radius: 6px;
margin: 5px 20px 5px 20px;
padding: 10px;
}
.host-item {
background-color: #1a1d29;
border: 1px solid #3a3f5c;
border-radius: 4px;
margin: 3px 2px;
padding: 6px 10px;
}
.active-title {
color: #2dce89;
font-weight: bold;
font-size: 14px;
}
.inactive-title {
color: #8892b0;
font-weight: bold;
font-size: 14px;
}
.customer-name {
color: #5e72e4;
font-weight: bold;
font-size: 14px;
}
.inactive-customer-name {
color: #8892b0;
font-weight: bold;
font-size: 14px;
}
.location-name {
color: #e8eaf6;
font-weight: bold;
font-size: 12px;
}
.vpn-type {
color: #8892b0;
font-size: 10px;
}
.connected-status {
color: #2dce89;
font-weight: bold;
font-size: 10px;
}
.disconnected-status {
color: #f5365c;
font-weight: bold;
font-size: 10px;
}
.connect-button {
background-color: #5e72e4;
color: white;
border: none;
border-radius: 4px;
padding: 5px 15px;
font-weight: bold;
}
.connect-button:hover {
background-color: #3a3f5c;
}
.disconnect-button {
background-color: #f5365c;
color: white;
border: none;
border-radius: 4px;
padding: 5px 15px;
font-weight: bold;
}
.disconnect-button:hover {
background-color: #3a3f5c;
}
.routes-button, .deactivate-button, .activate-button {
background-color: #3a3f5c;
color: #e8eaf6;
border: none;
border-radius: 4px;
padding: 5px 15px;
margin: 0 3px;
}
.deactivate-button {
background-color: #fb6340;
color: white;
}
.activate-button {
background-color: #5e72e4;
color: white;
font-weight: bold;
padding: 8px 20px;
}
.launch-button {
background-color: #5e72e4;
color: white;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-weight: bold;
font-size: 9px;
}
.host-name {
color: #e8eaf6;
font-weight: bold;
font-size: 9px;
}
.host-address {
color: #8892b0;
font-size: 8px;
font-family: monospace;
}
.services-header {
color: #5e72e4;
font-weight: bold;
font-size: 10px;
}
.service-count {
color: #6c757d;
font-size: 9px;
}
"""
css_provider.load_from_data(css.encode())
# Apply CSS to default screen
screen = Gdk.Screen.get_default()
style_context = Gtk.StyleContext()
style_context.add_provider_for_screen(
screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def setup_ui(self):
# Main container
main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
self.window.add(main_vbox)
# Header
header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
header_box.get_style_context().add_class("header")
main_vbox.pack_start(header_box, False, False, 0)
# Title with icon
title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
title_box.set_halign(Gtk.Align.CENTER)
header_box.pack_start(title_box, False, False, 10)
icon_label = Gtk.Label(label="🛡️")
icon_label.set_markup('<span font="24">🛡️</span>')
title_box.pack_start(icon_label, False, False, 0)
title_label = Gtk.Label(label="VPN Connection Manager")
title_label.get_style_context().add_class("title")
title_box.pack_start(title_label, False, False, 0)
# Search bar
search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
search_box.set_margin_start(20)
search_box.set_margin_end(20)
header_box.pack_start(search_box, False, False, 0)
search_icon = Gtk.Label(label="🔍")
search_box.pack_start(search_icon, False, False, 10)
self.search_entry = Gtk.Entry()
self.search_entry.set_placeholder_text("Search customers, locations, or hosts...")
self.search_entry.get_style_context().add_class("search-entry")
self.search_entry.connect("changed", self.filter_customers)
search_box.pack_start(self.search_entry, True, True, 0)
# Main content area with two columns
columns_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10, homogeneous=True)
columns_box.set_margin_start(10)
columns_box.set_margin_end(10)
columns_box.set_margin_bottom(10)
main_vbox.pack_start(columns_box, True, True, 0)
# Left column - Active customers
left_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
columns_box.pack_start(left_vbox, True, True, 0)
active_header = Gtk.Label(label="✅ Active Customers")
active_header.get_style_context().add_class("active-title")
active_header.set_halign(Gtk.Align.START)
left_vbox.pack_start(active_header, False, False, 0)
# Active customers scrolled window
active_scrolled = Gtk.ScrolledWindow()
active_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
left_vbox.pack_start(active_scrolled, True, True, 0)
self.active_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
active_scrolled.add(self.active_box)
# Right column - Inactive customers
right_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
columns_box.pack_start(right_vbox, True, True, 0)
inactive_header = Gtk.Label(label="💤 Inactive Customers")
inactive_header.get_style_context().add_class("inactive-title")
inactive_header.set_halign(Gtk.Align.START)
right_vbox.pack_start(inactive_header, False, False, 0)
# Inactive customers scrolled window
inactive_scrolled = Gtk.ScrolledWindow()
inactive_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
right_vbox.pack_start(inactive_scrolled, True, True, 0)
self.inactive_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
inactive_scrolled.add(self.inactive_box)
# Render initial data
self.render_customers()
def setup_system_tray(self):
self.indicator = AppIndicator3.Indicator.new(
"vpn-manager",
"network-vpn",
AppIndicator3.IndicatorCategory.APPLICATION_STATUS
)
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
# Create menu
menu = Gtk.Menu()
# Open item
open_item = Gtk.MenuItem(label="Open VPN Manager")
open_item.connect("activate", self.show_window_from_tray)
menu.append(open_item)
# Separator
menu.append(Gtk.SeparatorMenuItem())
# Quit item
quit_item = Gtk.MenuItem(label="Quit")
quit_item.connect("activate", self.quit_app)
menu.append(quit_item)
menu.show_all()
self.indicator.set_menu(menu)
def render_customers(self):
# Clear existing content
for child in self.active_box.get_children():
child.destroy()
for child in self.inactive_box.get_children():
child.destroy()
# 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_data = Customer(
name=customer.name,
locations=active_locations
)
customers_with_active.append(customer_data)
if inactive_locations:
customer_data = Customer(
name=customer.name,
locations=inactive_locations
)
customers_with_inactive.append(customer_data)
# Render active customers
for customer in customers_with_active:
self.create_customer_with_active_locations(customer)
# Render inactive customers
for customer in customers_with_inactive:
self.create_customer_without_active_locations(customer)
self.window.show_all()
def create_customer_with_active_locations(self, customer):
# Customer card
customer_frame = Gtk.Frame()
customer_frame.get_style_context().add_class("customer-card")
self.active_box.pack_start(customer_frame, False, False, 0)
customer_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
customer_frame.add(customer_vbox)
# Customer header
customer_label = Gtk.Label(label=f"🏢 {customer.name}")
customer_label.get_style_context().add_class("customer-name")
customer_label.set_halign(Gtk.Align.START)
customer_vbox.pack_start(customer_label, False, False, 0)
# Render each location
for location in customer.locations:
self.create_active_location_card(location, customer_vbox, customer.name)
def create_active_location_card(self, location, parent_box, customer_name):
# Location card
location_frame = Gtk.Frame()
location_frame.get_style_context().add_class("location-card")
parent_box.pack_start(location_frame, False, False, 0)
location_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
location_frame.add(location_vbox)
# Location header with controls
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
location_vbox.pack_start(header_box, False, False, 0)
# Location info
info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
header_box.pack_start(info_vbox, True, True, 0)
location_label = Gtk.Label(label=f"📍 {location.name}")
location_label.get_style_context().add_class("location-name")
location_label.set_halign(Gtk.Align.START)
info_vbox.pack_start(location_label, False, False, 0)
# VPN type
vpn_icons = {
"OpenVPN": "🔒",
"WireGuard": "",
"IPSec": "🛡️"
}
vpn_icon = vpn_icons.get(location.vpn_type, "🔑")
type_label = Gtk.Label(label=f"{vpn_icon} {location.vpn_type} VPN")
type_label.get_style_context().add_class("vpn-type")
type_label.set_halign(Gtk.Align.START)
info_vbox.pack_start(type_label, False, False, 0)
# Controls
controls_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
header_box.pack_end(controls_box, False, False, 0)
# Status
status_text = "● Connected" if location.connected else "○ Disconnected"
status_label = Gtk.Label(label=status_text)
if location.connected:
status_label.get_style_context().add_class("connected-status")
else:
status_label.get_style_context().add_class("disconnected-status")
controls_box.pack_start(status_label, False, False, 0)
# Connect/Disconnect button
btn_text = "Disconnect" if location.connected else "Connect"
connect_btn = Gtk.Button(label=btn_text)
if location.connected:
connect_btn.get_style_context().add_class("disconnect-button")
else:
connect_btn.get_style_context().add_class("connect-button")
connect_btn.connect("clicked", lambda btn, l=location: self.toggle_connection(l))
controls_box.pack_start(connect_btn, False, False, 0)
# Routes button
routes_btn = Gtk.Button(label="Routes")
routes_btn.get_style_context().add_class("routes-button")
routes_btn.connect("clicked", lambda btn, l=location: self.set_route(l))
controls_box.pack_start(routes_btn, False, False, 0)
# Deactivate button
deactivate_btn = Gtk.Button(label="Deactivate")
deactivate_btn.get_style_context().add_class("deactivate-button")
deactivate_btn.connect("clicked", lambda btn, l=location: self.deactivate_location(l, customer_name))
controls_box.pack_start(deactivate_btn, False, False, 0)
# Hosts section
if location.hosts:
# Separator
separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
location_vbox.pack_start(separator, False, False, 5)
services_label = Gtk.Label(label="💼 Available Services")
services_label.get_style_context().add_class("services-header")
services_label.set_halign(Gtk.Align.START)
location_vbox.pack_start(services_label, False, False, 0)
for host in location.hosts:
self.create_host_item(host, location_vbox)
def create_host_item(self, host, parent_box):
host_frame = Gtk.Frame()
host_frame.get_style_context().add_class("host-item")
parent_box.pack_start(host_frame, False, False, 0)
host_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
host_frame.add(host_box)
# Icon
type_icons = {
"SSH": "💻",
"Web": "🌐",
"SMB": "📂",
"PostgreSQL": "🗃️",
"Redis": "🗂️"
}
icon = type_icons.get(host.type, "📡")
icon_label = Gtk.Label(label=icon)
host_box.pack_start(icon_label, False, False, 0)
# Host details
details_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1)
host_box.pack_start(details_vbox, True, True, 0)
name_label = Gtk.Label(label=host.name)
name_label.get_style_context().add_class("host-name")
name_label.set_halign(Gtk.Align.START)
details_vbox.pack_start(name_label, False, False, 0)
addr_label = Gtk.Label(label=host.address)
addr_label.get_style_context().add_class("host-address")
addr_label.set_halign(Gtk.Align.START)
details_vbox.pack_start(addr_label, False, False, 0)
# Launch button for SSH and Web services
if host.type in ["SSH", "Web"]:
launch_btn = Gtk.Button(label="Launch")
launch_btn.get_style_context().add_class("launch-button")
launch_btn.connect("clicked", lambda btn, h=host: self.open_service(h))
host_box.pack_end(launch_btn, False, False, 0)
def create_customer_without_active_locations(self, customer):
# Customer card
customer_frame = Gtk.Frame()
customer_frame.get_style_context().add_class("customer-card")
self.inactive_box.pack_start(customer_frame, False, False, 0)
customer_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
customer_frame.add(customer_vbox)
# Customer header
customer_label = Gtk.Label(label=f"🏢 {customer.name}")
customer_label.get_style_context().add_class("inactive-customer-name")
customer_label.set_halign(Gtk.Align.START)
customer_vbox.pack_start(customer_label, False, False, 0)
# Render each location
for location in customer.locations:
self.create_inactive_location_card(location, customer_vbox, customer.name)
def create_inactive_location_card(self, location, parent_box, customer_name):
# Location card
location_frame = Gtk.Frame()
location_frame.get_style_context().add_class("location-card")
parent_box.pack_start(location_frame, False, False, 0)
location_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
location_frame.add(location_hbox)
# Location info
info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
location_hbox.pack_start(info_vbox, True, True, 0)
location_label = Gtk.Label(label=f"📍 {location.name}")
location_label.get_style_context().add_class("location-name")
location_label.set_halign(Gtk.Align.START)
info_vbox.pack_start(location_label, False, False, 0)
# VPN type
vpn_icons = {
"OpenVPN": "🔒",
"WireGuard": "",
"IPSec": "🛡️"
}
vpn_icon = vpn_icons.get(location.vpn_type, "🔑")
type_label = Gtk.Label(label=f"{vpn_icon} {location.vpn_type} VPN")
type_label.get_style_context().add_class("vpn-type")
type_label.set_halign(Gtk.Align.START)
info_vbox.pack_start(type_label, False, False, 0)
# Service count
service_count = len(location.hosts)
count_label = Gtk.Label(label=f"📊 {service_count} services available")
count_label.get_style_context().add_class("service-count")
count_label.set_halign(Gtk.Align.START)
info_vbox.pack_start(count_label, False, False, 0)
# Activate button
activate_btn = Gtk.Button(label="Set Active")
activate_btn.get_style_context().add_class("activate-button")
activate_btn.connect("clicked", lambda btn, l=location: self.set_location_active(l, customer_name))
location_hbox.pack_end(activate_btn, False, False, 0)
def set_location_active(self, location, customer_name):
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):
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, entry):
search_term = entry.get_text().lower()
if search_term:
self.filtered_customers = []
for customer in self.customers:
if search_term in customer.name.lower():
self.filtered_customers.append(customer)
else:
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:
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):
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, widget=None):
self.window.present()
self.window.show_all()
def hide_window(self, widget, event):
self.window.hide()
return True
def quit_app(self, widget=None):
Gtk.main_quit()
sys.exit(0)
def run(self):
self.window.show_all()
Gtk.main()
def main():
app = VPNManagerWindow()
app.run()
if __name__ == "__main__":
main()