Files
ClaudeProjectManager-main/gui/main_window.py
Claude Project Manager ec92da8a64 Initial commit
2025-07-07 22:11:38 +02:00

3120 Zeilen
137 KiB
Python

"""
Main Window GUI
The primary application window with project grid
"""
import customtkinter as ctk
from tkinter import filedialog, messagebox
import os
import threading
import subprocess
from typing import Optional
from PIL import Image
from gui.styles import COLORS, FONTS, WINDOW_CONFIG, TILE_SIZE, get_button_styles, get_colors
from gui.project_tile import ProjectTile, AddProjectTile
from gui.gitea_explorer import GiteaExplorer
from gui.sidebar_view import SidebarView
from gui.settings_dialog import SettingsDialog
from project_manager import ProjectManager, Project
from terminal_launcher import TerminalLauncher
from readme_generator import ReadmeGenerator
from vps_connection import VPSConnection
from process_manager import ProcessManager
from project_process_tracker import ProjectProcessTracker
from src.gitea.repository_manager import RepositoryManager
from pathlib import Path
from utils.logger import logger
class MainWindow:
def __init__(self):
# Load and set theme preference FIRST before anything else
self.load_and_apply_theme()
logger.info("Initializing Claude Project Manager")
# Setup global exception handler
self._setup_exception_handler()
# Configure customtkinter
ctk.set_default_color_theme("blue")
# Initialize components
logger.info("Initializing components")
self.project_manager = ProjectManager()
self.terminal_launcher = TerminalLauncher()
self.readme_generator = ReadmeGenerator()
self.vps_connection = VPSConnection()
self.process_manager = ProcessManager()
self.process_tracker = ProjectProcessTracker()
self.repo_manager = RepositoryManager()
# Set project manager reference
self.process_tracker.set_project_manager(self.project_manager)
# Store project tiles for updates
self.project_tiles = {}
self.selected_project_tile = None
# Interaction tracking
self.user_interacting = False
self.pending_updates = []
# Create main window
self.root = ctk.CTk()
self.root.title(WINDOW_CONFIG['title'])
self.root.geometry(f"{WINDOW_CONFIG['width']}x{WINDOW_CONFIG['height']}")
self.root.minsize(WINDOW_CONFIG['min_width'], WINDOW_CONFIG['min_height'])
# Make gitea_operation accessible from tiles
self.root.gitea_operation = self.gitea_operation
# Set window icon if available
try:
icon_path = os.path.join(os.path.dirname(__file__), "../icon.ico")
if os.path.exists(icon_path):
self.root.iconbitmap(icon_path)
except:
pass
# Initialize handlers AFTER root is created (Phase 2 refactoring)
self._init_handlers()
self.setup_ui()
self.refresh_projects()
logger.info("Claude Project Manager initialized successfully")
# Bind window resize event
self.root.bind('<Configure>', self.on_window_resize)
self.resize_timer = None
# Bind window close event
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# Bind interaction events
self.setup_interaction_tracking()
# Start periodic status check after a delay
self.root.after(30000, self.check_process_status) # Start after 30 seconds
# Initialize activity sync
self.init_activity_sync()
def _init_handlers(self):
"""Initialize refactored handlers (Phase 2/3 refactoring)"""
# Load feature flags from configuration
from gui.config import refactoring_config
self.refactoring_config = refactoring_config
self.REFACTORING_FLAGS = refactoring_config.flags
logger.info(f"Refactoring flags loaded: {self.REFACTORING_FLAGS}")
# Initialize handlers
try:
from gui.handlers.gitea_operations import GiteaOperationsHandler
from gui.handlers.process_manager import ProcessManagerHandler
from gui.handlers.project_manager import ProjectManagerHandler
from gui.handlers.ui_helpers import UIHelpersHandler
self._gitea_handler = GiteaOperationsHandler(self)
self._process_handler = ProcessManagerHandler(self)
self._project_handler = ProjectManagerHandler(self)
self._ui_handler = UIHelpersHandler(self)
logger.info("Refactoring handlers initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize handlers: {e}")
# Disable all flags if initialization fails
for key in self.REFACTORING_FLAGS:
self.REFACTORING_FLAGS[key] = False
def setup_ui(self):
"""Setup the main UI"""
# Main container
self.main_container = ctk.CTkFrame(self.root, fg_color=COLORS['bg_primary'])
self.main_container.pack(fill="both", expand=True)
# Header
self.create_header()
# Content area with scrollable frame
self.create_content_area()
# Status bar
self.create_status_bar()
def create_header(self):
"""Create header with title and toolbar - Facade method"""
if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False):
return self._ui_handler.create_header()
else:
return self._original_create_header()
def _original_create_header(self):
"""Create header with title and toolbar"""
header_frame = ctk.CTkFrame(self.main_container, fg_color=COLORS['bg_secondary'], height=80)
header_frame.pack(fill="x", padx=0, pady=0)
header_frame.pack_propagate(False)
# Title
self.title_label = ctk.CTkLabel(
header_frame,
text="IntelSight - Claude Project Manager",
font=FONTS['heading'],
text_color=COLORS['text_primary']
)
self.title_label.pack(side="left", padx=30, pady=20)
# Toolbar buttons
toolbar = ctk.CTkFrame(header_frame, fg_color="transparent")
toolbar.pack(side="right", padx=30, pady=20)
# WinSCP button
self.winscp_btn = ctk.CTkButton(
toolbar,
text="📁 WinSCP",
command=self.open_winscp,
width=100,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF",
font=('Segoe UI', 12)
)
self.winscp_btn.pack(side="left", padx=(0, 10))
# Log download button
self.log_btn = ctk.CTkButton(
toolbar,
text="📥 Log",
command=self.download_log,
width=80,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF",
font=('Segoe UI', 12)
)
self.log_btn.pack(side="left", padx=(0, 10))
# Refresh button
self.refresh_btn = ctk.CTkButton(
toolbar,
text="↻ Refresh",
command=self.refresh_projects,
width=100,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary']
)
self.refresh_btn.pack(side="left", padx=(0, 10))
# Settings button
self.settings_btn = ctk.CTkButton(
toolbar,
text="⚙️",
command=self.open_settings,
width=40,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary'],
font=('Segoe UI', 16)
)
self.settings_btn.pack(side="left")
def create_content_area(self):
"""Create content area with Gitea sidebar and project tiles"""
# Main content container
content_frame = ctk.CTkFrame(self.main_container, fg_color="transparent")
content_frame.pack(fill="both", expand=True)
# Left sidebar for SidebarView
self.sidebar_frame = ctk.CTkFrame(content_frame, fg_color=COLORS['bg_secondary'], width=300)
self.sidebar_frame.pack(side="left", fill="y", padx=(20, 10), pady=20)
self.sidebar_frame.pack_propagate(False)
# SidebarView with tabs/tree modes
self.sidebar_view = SidebarView(
self.sidebar_frame,
on_repo_select=self.on_gitea_repo_select
)
self.sidebar_view.pack(fill="both", expand=True)
self.sidebar_view.set_main_window(self) # Set reference for cross-selection
# Keep gitea_explorer reference for compatibility
self.gitea_explorer = self.sidebar_view.get_gitea_explorer()
# Right side for project tiles
tiles_container = ctk.CTkFrame(content_frame, fg_color="transparent")
tiles_container.pack(side="right", fill="both", expand=True, padx=(10, 20), pady=20)
# Scrollable frame container
self.scroll_container = ctk.CTkScrollableFrame(
tiles_container,
fg_color=COLORS['bg_primary']
)
self.scroll_container.pack(fill="both", expand=True)
# Flow frame for tiles (using pack instead of grid)
self.flow_frame = ctk.CTkFrame(self.scroll_container, fg_color="transparent")
self.flow_frame.pack(fill="both", expand=True)
def create_status_bar(self):
"""Create status bar at bottom - Facade method"""
if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False):
return self._ui_handler.create_status_bar()
else:
return self._original_create_status_bar()
def _original_create_status_bar(self):
"""Create status bar at bottom"""
self.status_bar = ctk.CTkFrame(self.main_container, fg_color=COLORS['bg_secondary'], height=30)
self.status_bar.pack(fill="x", side="bottom")
self.status_bar.pack_propagate(False)
self.status_label = ctk.CTkLabel(
self.status_bar,
text="Ready",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.status_label.pack(side="left", padx=20, pady=5)
# Activity status (clickable)
self.activity_frame = ctk.CTkFrame(self.status_bar, fg_color="transparent")
self.activity_frame.pack(side="left", padx=(20, 0))
self.activity_label = ctk.CTkLabel(
self.activity_frame,
text="",
font=FONTS['small'],
text_color=COLORS['accent_primary'],
cursor="hand2"
)
self.activity_label.pack(side="left")
self.activity_label.bind("<Button-1>", self.show_activity_details)
# Project count
self.count_label = ctk.CTkLabel(
self.status_bar,
text="0 projects",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.count_label.pack(side="right", padx=20, pady=5)
def refresh_projects(self, differential=False):
"""Refresh the project grid"""
# Skip if user is interacting
if self.user_interacting:
self.pending_updates.append(('refresh', None))
return
logger.info(f"Refreshing projects (differential: {differential})")
# Get all projects
projects = self.project_manager.get_all_projects()
# Update count
project_count = len([p for p in projects if p.id not in ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent"]])
self.count_label.configure(text=f"{project_count} project{'s' if project_count != 1 else ''}")
if differential and self.project_tiles:
# Differential update - only update changed tiles
self._differential_update(projects)
return
# Full refresh - clear and rebuild
for widget in self.flow_frame.winfo_children():
widget.destroy()
self.project_tiles.clear()
# Calculate how many tiles can fit in a row
window_width = self.scroll_container.winfo_width() if self.scroll_container.winfo_width() > 1 else WINDOW_CONFIG['width'] - 60
tile_width = TILE_SIZE['width']
tile_margin = 10
tiles_per_row = max(1, window_width // (tile_width + tile_margin * 2))
# Create row frames
current_row_frame = None
tiles_in_current_row = 0
# Helper function to create a new row
def create_new_row():
nonlocal current_row_frame, tiles_in_current_row
current_row_frame = ctk.CTkFrame(self.flow_frame, fg_color="transparent")
current_row_frame.pack(fill="x", pady=5)
tiles_in_current_row = 0
# Start with first row
create_new_row()
# Add VPS tile first
vps_project = next((p for p in projects if p.id == "vps-permanent"), None)
if vps_project:
self.create_project_tile_flow(vps_project, current_row_frame, is_vps=True)
tiles_in_current_row += 1
# Add Admin Panel tile second
admin_project = next((p for p in projects if p.id == "admin-panel-permanent"), None)
if admin_project:
if tiles_in_current_row >= tiles_per_row:
create_new_row()
self.create_project_tile_flow(admin_project, current_row_frame, is_vps=True)
tiles_in_current_row += 1
# Add VPS Docker tile third
vps_docker_project = next((p for p in projects if p.id == "vps-docker-permanent"), None)
if vps_docker_project:
if tiles_in_current_row >= tiles_per_row:
create_new_row()
self.create_project_tile_flow(vps_docker_project, current_row_frame, is_vps=True)
tiles_in_current_row += 1
# Add project tiles
for project in projects:
if project.id not in ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent"]:
if tiles_in_current_row >= tiles_per_row:
create_new_row()
self.create_project_tile_flow(project, current_row_frame)
tiles_in_current_row += 1
# Add "Add Project" tile
if tiles_in_current_row >= tiles_per_row:
create_new_row()
self.create_add_tile_flow(current_row_frame)
self.update_status("Projects refreshed")
def create_project_tile(self, project: Project, row: int, col: int, is_vps: bool = False):
"""Create a project tile (legacy grid method)"""
# This method is kept for compatibility but not used
pass
def create_add_tile(self, row: int, col: int):
"""Create add project tile (legacy grid method)"""
# This method is kept for compatibility but not used
pass
def create_project_tile_flow(self, project: Project, parent_frame, is_vps: bool = False):
"""Create a project tile in flow layout"""
is_running = self.process_tracker.is_running(project.id)
tile = ProjectTile(
parent_frame,
project,
on_open=self.open_project,
on_readme=self.open_readme,
on_delete=self.delete_project if not is_vps else None,
on_stop=self.stop_project,
is_vps=is_vps,
is_running=is_running,
on_rename=self.rename_project if not is_vps else None,
on_select=self.on_project_select
)
# Set main window reference for activity methods
tile.main_window = self
tile.pack(side="left", padx=10, pady=10)
self.project_tiles[project.id] = tile
def create_add_tile_flow(self, parent_frame):
"""Create add project tile in flow layout"""
add_tile = AddProjectTile(parent_frame, on_add=self.add_new_project)
add_tile.pack(side="left", padx=10, pady=10)
def add_new_project(self):
"""Add a new project"""
logger.info("Adding new project")
# Open folder dialog
folder_path = filedialog.askdirectory(
title="Select Project Folder"
)
if folder_path:
# Get project name from folder
project_name = os.path.basename(folder_path)
logger.info(f"Selected project folder: {folder_path}")
# Add to manager
project = self.project_manager.add_project(project_name, folder_path)
# Generate README in background
threading.Thread(
target=self.generate_readme_background,
args=(project,),
daemon=True
).start()
# Don't launch Claude automatically - just add the project
self.update_status(f"Added project: {project_name}")
logger.info(f"Successfully added project: {project_name}")
# Refresh grid with differential update
self.refresh_projects(differential=True)
def open_project(self, project: Project):
"""Open a project in Claude"""
logger.info(f"Opening project: {project.name}")
if project.id == "vps-permanent":
# Handle VPS connection
self.open_vps_connection()
elif project.id == "admin-panel-permanent":
# Handle Admin Panel
self.open_admin_panel()
elif project.id == "vps-docker-permanent":
# Handle VPS Docker
self.open_vps_docker()
else:
# Check if already running
if self.process_tracker.is_running(project.id):
self.update_status(f"Claude already running for: {project.name}", error=True)
return
# Normal project
process = self.terminal_launcher.launch_claude_wsl(project.path, project.name)
if process:
# Track the process
self.process_manager.processes[project.id] = process
self.process_manager.save_process_data()
self.process_tracker.set_running(project.id)
# Update tile status immediately
if project.id in self.project_tiles:
self.project_tiles[project.id].update_status(True)
self.update_status(f"Started: {project.name}")
# Update last accessed
project.update_last_accessed()
self.project_manager.save_projects()
# Update README in background
threading.Thread(
target=self.generate_readme_background,
args=(project,),
daemon=True
).start()
# Start monitoring the process
self.monitor_process(project.id, process)
else:
self.update_status(f"Failed to start: {project.name}", error=True)
# Re-enable button if launch failed
if project.id in self.project_tiles:
self.project_tiles[project.id].update_status(False)
def open_vps_connection(self):
"""Open VPS connection"""
# Check if already running
if self.process_tracker.is_running("vps-permanent"):
self.update_status("VPS connection already active", error=True)
return
# Create VPS README if needed
vps_readme_dir = os.path.join(os.path.dirname(__file__), "../data/vps_readme")
os.makedirs(vps_readme_dir, exist_ok=True)
vps_readme_path = os.path.join(vps_readme_dir, "VPS_README.md")
# Generate VPS README
self.vps_connection.generate_vps_readme(vps_readme_path)
# Create connection script
script = self.vps_connection.create_ssh_script()
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
f.write(script)
script_path = f.name
# Launch terminal
import subprocess
if os.path.exists(r"C:\Windows\System32\wt.exe"):
process = subprocess.Popen([
"wt.exe", "-w", "0", "new-tab",
"--title", "Claude VPS Server",
"--", "cmd", "/c", script_path
])
else:
process = subprocess.Popen(['cmd', '/c', 'start', 'Claude VPS', script_path])
# Track the process
self.process_manager.processes["vps-permanent"] = process
self.process_manager.save_process_data()
self.process_tracker.set_running("vps-permanent")
# Update tile status immediately
if "vps-permanent" in self.project_tiles:
self.project_tiles["vps-permanent"].update_status(True)
self.update_status("Connected to VPS server")
# Update VPS project
vps_project = self.project_manager.get_project("vps-permanent")
if vps_project:
vps_project.update_last_accessed()
self.project_manager.save_projects()
# Monitor the process
self.monitor_process("vps-permanent", process)
def open_admin_panel(self):
"""Open Admin Panel via VPS connection with directory change"""
# Check if already running
if self.process_tracker.is_running("admin-panel-permanent"):
self.update_status("Admin Panel already running", error=True)
return
# Create connection script for Admin Panel
script = self.vps_connection.create_admin_panel_script()
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
f.write(script)
script_path = f.name
# Launch terminal
import subprocess
if os.path.exists(r"C:\Windows\System32\wt.exe"):
process = subprocess.Popen([
"wt.exe", "-w", "0", "new-tab",
"--title", "Claude Admin Panel",
"--", "cmd", "/c", script_path
])
else:
process = subprocess.Popen(['cmd', '/c', 'start', 'Claude Admin Panel', script_path])
# Track the process
self.process_manager.processes["admin-panel-permanent"] = process
self.process_manager.save_process_data()
self.process_tracker.set_running("admin-panel-permanent")
# Update tile status immediately
if "admin-panel-permanent" in self.project_tiles:
self.project_tiles["admin-panel-permanent"].update_status(True)
self.update_status("Connected to Admin Panel")
# Update Admin Panel project
admin_project = self.project_manager.get_project("admin-panel-permanent")
if admin_project:
admin_project.update_last_accessed()
self.project_manager.save_projects()
# Monitor the process
self.monitor_process("admin-panel-permanent", process)
def open_vps_docker(self):
"""Open VPS Docker connection for Admin Panel restart"""
# Check if already running
if self.process_tracker.is_running("vps-docker-permanent"):
self.update_status("VPS Docker restart already running", error=True)
return
# Create connection script for VPS Docker
script = self.vps_connection.create_vps_docker_script()
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
f.write(script)
script_path = f.name
# Launch terminal
import subprocess
if os.path.exists(r"C:\Windows\System32\wt.exe"):
process = subprocess.Popen([
"wt.exe", "-w", "0", "new-tab",
"--title", "VPS Docker Restart",
"--", "cmd", "/c", script_path
])
else:
process = subprocess.Popen(['cmd', '/c', 'start', 'VPS Docker', script_path])
# Track the process
self.process_manager.processes["vps-docker-permanent"] = process
self.process_manager.save_process_data()
self.process_tracker.set_running("vps-docker-permanent")
# Update tile status immediately
if "vps-docker-permanent" in self.project_tiles:
self.project_tiles["vps-docker-permanent"].update_status(True)
self.update_status("Starting VPS Docker Admin Panel restart")
# Update VPS Docker project
vps_docker_project = self.project_manager.get_project("vps-docker-permanent")
if vps_docker_project:
vps_docker_project.update_last_accessed()
self.project_manager.save_projects()
# Monitor the process
self.monitor_process("vps-docker-permanent", process)
def open_readme(self, project: Project):
"""Open project README"""
if project.id == "vps-permanent":
# VPS README
readme_path = os.path.join(os.path.dirname(__file__), "../data/vps_readme/VPS_README.md")
if os.path.exists(readme_path):
os.startfile(readme_path)
else:
self.update_status("VPS README not found", error=True)
elif project.id == "admin-panel-permanent":
# Admin Panel doesn't have a README - just show a message
self.update_status("Admin Panel has no README file")
elif project.id == "vps-docker-permanent":
# VPS Docker doesn't have a README - just show a message
self.update_status("VPS Docker has no README file")
else:
# Project README
readme_path = os.path.join(project.path, "CLAUDE_PROJECT_README.md")
if not os.path.exists(readme_path):
# Generate if doesn't exist
self.update_status("Generating README...")
self.readme_generator.generate_and_save_readme(project.path, project.name)
# Open with default editor
try:
os.startfile(readme_path)
self.update_status(f"Opened README: {project.name}")
except:
self.update_status(f"Could not open README", error=True)
def delete_project(self, project: Project):
"""Delete a project from manager (not files) - Facade method"""
if hasattr(self, '_project_handler') and self.REFACTORING_FLAGS.get('USE_PROJECT_HANDLER', False):
return self._project_handler.delete_project(project)
else:
return self._original_delete_project(project)
def _original_delete_project(self, project: Project):
"""Delete a project from manager (not files)"""
logger.info(f"Attempting to delete project: {project.name}")
if messagebox.askyesno("Projekt löschen",
f"Möchten Sie das Projekt '{project.name}' wirklich aus dem Projekt-Manager entfernen?\n\n"
"Hinweis: Die Dateien werden NICHT gelöscht."):
self.project_manager.remove_project(project.id)
self.refresh_projects()
self.update_status(f"Removed: {project.name}")
def rename_project(self, project: Project):
"""Rename a project's display name"""
# Create simple dialog
dialog = ctk.CTkToplevel(self.root)
dialog.title("Anzeigename ändern")
dialog.geometry("400x150")
dialog.configure(fg_color=COLORS['bg_primary'])
# Center the dialog
dialog.transient(self.root)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 400) // 2
y = (dialog.winfo_screenheight() - 150) // 2
dialog.geometry(f"400x150+{x}+{y}")
# Make dialog grab focus
dialog.grab_set()
dialog.lift()
dialog.attributes('-topmost', True)
# Label
label = ctk.CTkLabel(
dialog,
text=f"Neuer Anzeigename für '{project.name}':",
font=FONTS['tile_text'],
text_color=COLORS['text_primary']
)
label.pack(pady=(20, 10))
# Entry
entry = ctk.CTkEntry(
dialog,
width=350,
font=FONTS['tile_text'],
fg_color=COLORS['bg_secondary'],
text_color=COLORS['text_primary']
)
entry.pack(pady=5)
entry.insert(0, project.name)
entry.select_range(0, 'end')
# Force focus after a small delay
dialog.after(100, lambda: entry.focus_force())
# Buttons
button_frame = ctk.CTkFrame(dialog, fg_color="transparent")
button_frame.pack(pady=15)
def save_rename():
new_name = entry.get().strip()
if new_name and new_name != project.name:
# Update project name
self.project_manager.update_project(project.id, name=new_name)
# Update tile
if project.id in self.project_tiles:
self.project_tiles[project.id].update_project(
self.project_manager.get_project(project.id)
)
self.update_status(f"Renamed to: {new_name}")
dialog.grab_release()
dialog.destroy()
# Save on Enter
entry.bind("<Return>", lambda e: save_rename())
save_btn = ctk.CTkButton(
button_frame,
text="Speichern",
command=save_rename,
width=100,
**get_button_styles()['primary']
)
save_btn.pack(side="left", padx=5)
cancel_btn = ctk.CTkButton(
button_frame,
text="Abbrechen",
command=lambda: [dialog.grab_release(), dialog.destroy()],
width=100,
**get_button_styles()['secondary']
)
cancel_btn.pack(side="left", padx=5)
# Bind Escape key to cancel
dialog.bind("<Escape>", lambda e: [dialog.grab_release(), dialog.destroy()])
def generate_readme_background(self, project: Project):
"""Generate README in background thread"""
try:
self.readme_generator.generate_and_save_readme(project.path, project.name)
except Exception as e:
print(f"Error generating README: {e}")
def update_status(self, message: str, error: bool = False):
"""Update status bar message - Facade method"""
if hasattr(self, '_process_handler') and self.REFACTORING_FLAGS.get('USE_PROCESS_HANDLER', False):
return self._process_handler.update_status(message, error)
else:
return self._original_update_status(message, error)
def _original_update_status(self, message: str, error: bool = False):
"""Update status bar message"""
self.status_label.configure(
text=message,
text_color=COLORS['accent_error'] if error else COLORS['text_secondary']
)
def download_log(self):
"""Download application log file"""
logger.info("Log download button clicked")
# Always use the original implementation for now
return self._original_download_log()
def _original_download_log(self):
"""Download application log file"""
try:
logger.info("Opening file dialog for log export")
# Create default filename
from datetime import datetime
default_filename = f"CPM_Log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
# Open file dialog to choose save location
file_path = filedialog.asksaveasfilename(
defaultextension=".log",
filetypes=[("Log files", "*.log"), ("Text files", "*.txt"), ("All files", "*.*")],
initialfile=default_filename
)
if file_path:
logger.info(f"Exporting log to: {file_path}")
# Export logs to chosen location
logger.export_logs(file_path)
self.update_status(f"Log saved to: {os.path.basename(file_path)}")
logger.info(f"Log exported successfully to: {file_path}")
messagebox.showinfo("Export erfolgreich", f"Log wurde gespeichert:\n{os.path.basename(file_path)}")
else:
logger.info("Log export cancelled by user")
except Exception as e:
error_msg = f"Error saving log: {str(e)}"
self.update_status(error_msg, error=True)
logger.error(error_msg, exc_info=True)
messagebox.showerror("Export Error", error_msg)
def open_settings(self):
"""Open settings dialog"""
settings_dialog = SettingsDialog(self.root, self.sidebar_view)
def open_winscp(self):
"""Open WinSCP with VPS connection"""
try:
import os
import tempfile
from pathlib import Path
# VPS connection details
vps_host = "91.99.192.14"
vps_username = "claude-dev"
vps_password = "z0E1Al}q2H?Yqd!O"
# Check if WinSCP is in tools folder
winscp_portable = Path(__file__).parent.parent / "tools" / "WinSCP" / "WinSCP.exe"
# Alternative: Check if WinSCP is installed
winscp_installed = r"C:\Program Files (x86)\WinSCP\WinSCP.exe"
winscp_installed_64 = r"C:\Program Files\WinSCP\WinSCP.exe"
winscp_path = None
if winscp_portable.exists():
winscp_path = str(winscp_portable)
logger.info(f"Using portable WinSCP: {winscp_path}")
elif os.path.exists(winscp_installed):
winscp_path = winscp_installed
logger.info(f"Using installed WinSCP (x86): {winscp_path}")
elif os.path.exists(winscp_installed_64):
winscp_path = winscp_installed_64
logger.info(f"Using installed WinSCP (x64): {winscp_path}")
else:
# WinSCP not found
from tkinter import messagebox
messagebox.showwarning(
"WinSCP nicht gefunden",
"WinSCP wurde nicht gefunden.\n\n"
"Bitte installieren Sie WinSCP von:\n"
"https://winscp.net/download/WinSCP-Portable.zip\n\n"
"Oder entpacken Sie WinSCP Portable in:\n"
f"{winscp_portable.parent}"
)
return
# Create WinSCP session URL
# Format: sftp://username:password@hostname/
session_url = f"sftp://{vps_username}:{vps_password}@{vps_host}/"
# Start WinSCP with session URL
cmd = f'"{winscp_path}" "{session_url}"'
logger.info(f"Starting WinSCP with VPS connection")
import subprocess
subprocess.Popen(cmd, shell=True)
self.update_status("WinSCP gestartet")
except Exception as e:
error_msg = f"Fehler beim Starten von WinSCP: {str(e)}"
self.update_status(error_msg, error=True)
logger.error(error_msg)
messagebox.showerror("WinSCP Fehler", error_msg)
def refresh_ui(self):
"""Refresh the entire UI"""
# Import updated styles
from gui.styles import COLORS, BUTTON_STYLES
# Update main container
self.main_container.configure(fg_color=COLORS['bg_primary'])
# Update header
for widget in self.main_container.winfo_children():
if isinstance(widget, ctk.CTkFrame):
widget.configure(fg_color=COLORS['bg_secondary'] if widget.winfo_y() < 100 else COLORS['bg_primary'])
# Update sidebar frame
self.sidebar_frame.configure(fg_color=COLORS['bg_secondary'])
# Update scroll container
self.scroll_container.configure(fg_color=COLORS['bg_primary'])
# Update status bar
self.status_bar.configure(fg_color=COLORS['bg_secondary'])
self.status_label.configure(text_color=COLORS['text_secondary'])
self.count_label.configure(text_color=COLORS['text_secondary'])
# Update title label
self.title_label.configure(text_color=COLORS['text_primary'])
# Update log button
self.log_btn.configure(
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF"
)
# Update refresh button
self.refresh_btn.configure(
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary']
)
# Update Gitea Explorer colors
self.gitea_explorer.refresh_colors()
# Update existing project tiles without full refresh
self._update_project_tiles_colors()
def load_and_apply_theme(self):
"""Apply dark mode - Facade method"""
if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False):
return self._ui_handler.load_and_apply_theme()
else:
return self._original_load_and_apply_theme()
def _original_load_and_apply_theme(self):
"""Apply dark mode (only mode available)"""
# Always use dark mode
ctk.set_appearance_mode('dark')
def on_window_resize(self, event):
"""Handle window resize events - Facade method"""
if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False):
return self._ui_handler.on_window_resize(event)
else:
return self._original_on_window_resize(event)
def _original_on_window_resize(self, event):
"""Handle window resize events"""
# Only process resize events from the main window
if event.widget == self.root:
# Cancel previous timer
if self.resize_timer:
self.root.after_cancel(self.resize_timer)
# Set new timer to refresh after resize stops with differential update
self.resize_timer = self.root.after(300, lambda: self.refresh_projects(differential=True))
def stop_project(self, project: Project):
"""Stop a running project"""
logger.info(f"Stopping project: {project.name}")
# Always try to stop, even if process manager doesn't have it tracked
self.process_manager.stop_process(project.id, project.name)
self.process_tracker.set_stopped(project.id)
self.update_status(f"Stopped: {project.name}")
# Update tile status
if project.id in self.project_tiles:
self.project_tiles[project.id].update_status(False)
def _differential_update(self, projects):
"""Update only changed project tiles"""
project_dict = {p.id: p for p in projects}
existing_ids = set(self.project_tiles.keys())
new_ids = set(project_dict.keys())
# Remove tiles for deleted projects
for removed_id in existing_ids - new_ids:
if removed_id in self.project_tiles:
self.project_tiles[removed_id].destroy()
del self.project_tiles[removed_id]
# Update existing tiles
for project_id in existing_ids & new_ids:
project = project_dict[project_id]
tile = self.project_tiles[project_id]
# Update project data
tile.update_project(project)
# Update running status
is_running = self.process_tracker.is_running(project_id)
if tile.is_running != is_running:
tile.update_status(is_running)
# Add new tiles (this is more complex due to flow layout)
# For now, trigger full refresh if new projects are added
if new_ids - existing_ids:
self.refresh_projects(differential=False)
def _update_project_tiles_colors(self):
"""Update colors of existing project tiles without recreating them"""
from gui.styles import COLORS, BUTTON_STYLES
# Update each project tile's colors
for tile in self.project_tiles.values():
if hasattr(tile, 'is_vps') and not tile.is_vps:
# Update regular project tile colors
tile.configure(fg_color=COLORS['bg_tile'])
# Update labels
if hasattr(tile, 'title_label'):
tile.title_label.configure(text_color=COLORS['text_primary'])
if hasattr(tile, 'path_label'):
tile.path_label.configure(text_color=COLORS['text_secondary'])
if hasattr(tile, 'tags_label'):
tile.tags_label.configure(text_color=COLORS['text_dim'])
if hasattr(tile, 'time_label'):
tile.time_label.configure(text_color=COLORS['text_dim'])
if hasattr(tile, 'status_indicator'):
tile.status_indicator.configure(
text_color=COLORS['accent_success'] if tile.is_running else COLORS['accent_error']
)
if hasattr(tile, 'status_message'):
tile.status_message.configure(text_color=COLORS['accent_warning'])
# Update button styles
if hasattr(tile, 'open_button'):
btn_style = BUTTON_STYLES['danger'] if tile.is_running else BUTTON_STYLES['primary']
tile.open_button.configure(
fg_color=btn_style['fg_color'],
hover_color=btn_style['hover_color'],
text_color=btn_style.get('text_color', '#FFFFFF')
)
if hasattr(tile, 'gitea_button'):
tile.gitea_button.configure(
fg_color=BUTTON_STYLES['secondary']['fg_color'],
hover_color=BUTTON_STYLES['secondary']['hover_color'],
text_color=BUTTON_STYLES['secondary']['text_color']
)
if hasattr(tile, 'explorer_button'):
tile.explorer_button.configure(
fg_color=BUTTON_STYLES['secondary']['fg_color'],
hover_color=BUTTON_STYLES['secondary']['hover_color'],
text_color=BUTTON_STYLES['secondary']['text_color']
)
if hasattr(tile, 'delete_button'):
tile.delete_button.configure(
fg_color=COLORS['bg_secondary'],
hover_color=COLORS['accent_error'],
text_color=COLORS['text_secondary']
)
# Update Add Project Tile if it exists
for widget in self.flow_frame.winfo_children():
if isinstance(widget, ctk.CTkFrame):
for child in widget.winfo_children():
if child.__class__.__name__ == 'AddProjectTile':
child.configure(
fg_color=COLORS['bg_secondary'],
border_color=COLORS['border_primary']
)
# Update nested labels in AddProjectTile
for subwidget in child.winfo_children():
if isinstance(subwidget, ctk.CTkFrame):
for label in subwidget.winfo_children():
if isinstance(label, ctk.CTkLabel):
if label.cget("font")[1] == 48: # Folder icon
label.configure(text_color=COLORS['text_dim'])
elif "Add New Project" in label.cget("text"):
label.configure(text_color=COLORS['text_secondary'])
elif "Click to select" in label.cget("text"):
label.configure(text_color=COLORS['text_dim'])
def setup_interaction_tracking(self):
"""Setup tracking for user interactions"""
# Track when dropdowns or menus are active
self.root.bind_all("<Button-1>", self._on_click_start)
self.root.bind_all("<ButtonRelease-1>", self._on_click_end)
self.root.bind_all("<<ComboboxSelected>>", self._on_dropdown_select)
self.root.bind_all("<FocusIn>", self._on_focus_in)
self.root.bind_all("<FocusOut>", self._on_focus_out)
def _on_click_start(self, event):
"""Track start of user interaction"""
self.user_interacting = True
def _on_click_end(self, event):
"""Track end of user interaction"""
# Delay to allow dropdown/menu operations to complete
self.root.after(500, self._check_pending_updates)
def _on_dropdown_select(self, event):
"""Handle dropdown selection"""
self.user_interacting = True
self.root.after(500, self._check_pending_updates)
def _on_focus_in(self, event):
"""Track focus events"""
if isinstance(event.widget, (ctk.CTkComboBox, ctk.CTkOptionMenu)):
self.user_interacting = True
def _on_focus_out(self, event):
"""Track focus loss"""
self.root.after(200, self._check_pending_updates)
def _check_pending_updates(self):
"""Process pending updates after interaction ends"""
self.user_interacting = False
# Process any pending updates
if self.pending_updates:
updates = self.pending_updates[:]
self.pending_updates.clear()
# Process unique update types
update_types = set(u[0] for u in updates)
if 'refresh' in update_types:
self.refresh_projects(differential=True)
elif 'status_check' in update_types:
# Run status check immediately
self.check_process_status()
def monitor_process(self, project_id: str, process: subprocess.Popen):
"""Monitor a process and update status when it ends"""
def check_process():
try:
# Wait for process to complete
process.wait()
# Process has ended, update status
self.root.after(0, self._handle_process_ended, project_id)
except:
pass
# Start monitoring in background thread
monitor_thread = threading.Thread(target=check_process, daemon=True)
monitor_thread.start()
def _handle_process_ended(self, project_id: str):
"""Handle when a process ends (called in main thread)"""
# Update tracking
self.process_tracker.set_stopped(project_id)
if project_id in self.process_manager.processes:
del self.process_manager.processes[project_id]
self.process_manager.save_process_data()
# Update tile status
if project_id in self.project_tiles:
self.project_tiles[project_id].update_status(False)
# Update status bar
project = self.project_manager.get_project(project_id)
if project:
self.update_status(f"Claude closed: {project.name}")
def check_process_status(self):
"""Periodically check the status of all processes"""
# Skip if user is interacting
if self.user_interacting:
self.pending_updates.append(('status_check', None))
# Schedule next check
self.root.after(30000, self.check_process_status)
return
# Only check if we have any supposedly running projects
if any(tile.is_running for tile in self.project_tiles.values()):
# Check each running project tile
for project_id, tile in self.project_tiles.items():
if tile.is_running: # Only check running projects
actual_running = self.process_tracker.is_running(project_id)
if not actual_running:
# Only update the specific tile, not the entire UI
tile.update_status(False)
self.process_tracker.set_stopped(project_id)
# Schedule next check in 30 seconds (less frequent)
self.root.after(30000, self.check_process_status)
def open_gitea_window(self):
"""Open Gitea integration window"""
# Create Gitea window if not already open
gitea_window = ctk.CTkToplevel(self.root)
gitea_window.title("Gitea Integration")
gitea_window.geometry("1200x700")
gitea_window.configure(fg_color=COLORS['bg_primary'])
# Center the window
gitea_window.transient(self.root)
gitea_window.update_idletasks()
x = (gitea_window.winfo_screenwidth() - 1200) // 2
y = (gitea_window.winfo_screenheight() - 700) // 2
gitea_window.geometry(f"1200x700+{x}+{y}")
# Create frame for Gitea UI
gitea_frame = ctk.CTkFrame(gitea_window, fg_color=COLORS['bg_primary'])
gitea_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Import and create Gitea UI
try:
from src.gitea.gitea_ui_ctk import GiteaIntegrationUI
# Initialize Gitea UI with the window
gitea_ui = GiteaIntegrationUI(gitea_window)
self.update_status("Gitea Integration opened")
except Exception as e:
import traceback
traceback.print_exc()
messagebox.showerror("Error", f"Failed to open Gitea Integration: {e}")
gitea_window.destroy()
def on_gitea_repo_select(self, repo):
"""Handle Gitea repository selection"""
logger.info(f"Gitea repo selected: {repo.get('name', 'Unknown')}")
# Clear project tile selection when selecting a Gitea repo
if hasattr(self, 'selected_project_tile') and self.selected_project_tile:
self.selected_project_tile.set_selected(False)
self.selected_project_tile = None
self.selected_project = None
# No toolbar needed anymore - using dropdown in tiles
def clear_project_selection(self):
"""Clear the current project tile selection"""
if self.selected_project_tile:
self.selected_project_tile.set_selected(False)
self.selected_project_tile = None
self.selected_project = None
# Show toolbar for Gitea repo
status_text = f"Repository: {repo['name']}"
if repo.get('private'):
status_text += " (Privat)"
self.gitea_toolbar.show_for_context('gitea_repo', repo, status_text)
def on_project_select(self, project):
"""Handle project tile selection"""
self.selected_project = project
# Update visual selection
if self.selected_project_tile and project.id in self.project_tiles:
# Deselect previous tile
self.selected_project_tile.set_selected(False)
# Select new tile
if project.id in self.project_tiles:
self.selected_project_tile = self.project_tiles[project.id]
self.selected_project_tile.set_selected(True)
# Clear Gitea selection when project is selected
if hasattr(self, 'gitea_explorer'):
self.gitea_explorer.clear_selection()
# No toolbar anymore - using dropdown buttons
# Git operation callbacks
def show_git_status(self, item):
"""Show git status for project or repo"""
if isinstance(item, Project):
# For local project
try:
# Hide console window on Windows
startupinfo = None
if os.name == 'nt': # Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
["git", "status", "--porcelain"],
cwd=item.path,
capture_output=True,
text=True,
startupinfo=startupinfo
)
if result.stdout:
messagebox.showinfo("Git Status", f"Uncommitted changes:\n{result.stdout}")
else:
messagebox.showinfo("Git Status", "Working directory clean")
except Exception as e:
messagebox.showerror("Error", f"Failed to get git status: {e}")
else:
# For Gitea repo
messagebox.showinfo("Repository Info",
f"Name: {item['name']}\n"
f"Private: {'Yes' if item.get('private') else 'No'}\n"
f"Description: {item.get('description', 'No description')}")
def commit_changes(self, project):
"""Commit changes for a project"""
from tkinter import simpledialog
# Check for changes
git_ops = self.repo_manager.git_ops
success, status = git_ops.status(Path(project.path))
if not status.strip():
messagebox.showinfo("Keine Änderungen", "Keine Änderungen zum Committen vorhanden.")
return
# Ask for commit message
message = simpledialog.askstring(
"Commit",
"Commit-Nachricht eingeben:",
parent=self.root
)
if message:
# Add all changes
git_ops.add(Path(project.path))
# Commit
success, result = git_ops.commit(Path(project.path), message)
if success:
messagebox.showinfo("Erfolg", "Änderungen erfolgreich committet!")
else:
messagebox.showerror("Fehler", f"Commit fehlgeschlagen: {result}")
def push_to_gitea(self, project):
"""Push project to Gitea - Facade method"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.push_to_gitea(project)
else:
return self._original_push_to_gitea(project)
def _original_push_to_gitea(self, project):
"""Push project to Gitea"""
import logging
logger = logging.getLogger(__name__)
project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path'])
project_name = project.name if hasattr(project, 'name') else project.get('name', 'Unknown')
# Check current remote
git_ops = self.repo_manager.git_ops
success, remotes = git_ops.remote_list(project_path)
if success:
logger.info(f"Current remotes before push: {remotes}")
# Check for large files before push
large_files = git_ops.check_large_files(project_path, 50) # 50MB limit
if large_files:
msg = "⚠️ Große Dateien gefunden!\n\n"
msg += "Folgende Dateien überschreiten 50MB:\n\n"
total_size = 0
for file, size in large_files[:10]: # Show first 10
size_mb = size / (1024 * 1024)
msg += f"{file} ({size_mb:.1f} MB)\n"
total_size += size
if len(large_files) > 10:
msg += f" ... und {len(large_files) - 10} weitere\n"
total_mb = total_size / (1024 * 1024)
msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n"
# Simple solution: offer to exclude files
msg += "Diese großen Dateien verhindern den Push zu Gitea.\n\n"
msg += "EMPFEHLUNG: Große Dateien aus Git entfernen\n\n"
msg += "Möchten Sie diese Dateien aus Git entfernen?\n"
msg += "(Die Dateien bleiben auf Ihrer Festplatte erhalten)"
if messagebox.askyesno("Große Dateien gefunden", msg, icon='warning'):
# Remove large files from git but keep them locally
try:
# First create .gitignore
gitignore_path = project_path / ".gitignore"
existing_content = ""
if gitignore_path.exists():
with open(gitignore_path, 'r', encoding='utf-8') as f:
existing_content = f.read()
# Add large files to .gitignore
with open(gitignore_path, 'a', encoding='utf-8') as f:
if existing_content and not existing_content.endswith('\n'):
f.write('\n')
f.write("\n# Große Dateien (automatisch hinzugefügt)\n")
for file, _ in large_files:
f.write(f"{file}\n")
# Remove files from git index but keep local copies
removed_count = 0
for file, _ in large_files:
cmd = ["git", "rm", "--cached", file]
success, _, _ = git_ops._run_git_command(cmd, cwd=project_path)
if success:
removed_count += 1
if removed_count > 0:
# Commit the changes
git_ops.add(Path(project_path), [".gitignore"])
success, _ = git_ops.commit(Path(project_path),
f"Große Dateien aus Git entfernt ({removed_count} Dateien)")
if success:
messagebox.showinfo("Erfolg",
f"{removed_count} große Dateien wurden aus Git entfernt.\n\n"
"Die Dateien sind weiterhin lokal vorhanden.\n"
"Sie können jetzt den Push erneut versuchen.")
# Continue with push
success, result = git_ops.push(project_path)
if success:
messagebox.showinfo("Push erfolgreich", "Änderungen wurden zu Gitea gepusht!")
return
else:
messagebox.showerror("Push fehlgeschlagen", f"Push fehlgeschlagen: {result}")
return
else:
messagebox.showerror("Fehler", "Konnte Änderungen nicht committen.")
return
except Exception as e:
messagebox.showerror("Fehler", f"Fehler beim Entfernen der Dateien: {str(e)}")
return
else:
# User wants alternative solution
if messagebox.askyesno("Alternative",
"Möchten Sie die großen Dateien manuell bearbeiten?\n\n"
"Optionen:\n"
"- Dateien komprimieren (z.B. ZIP)\n"
"- Dateien auf externen Speicher verschieben\n"
"- Dateien in kleinere Teile aufteilen\n\n"
"Soll ich eine .gitignore-Datei erstellen?"):
gitignore_path = project_path / ".gitignore"
try:
# Read existing .gitignore if exists
existing_content = ""
if gitignore_path.exists():
with open(gitignore_path, 'r', encoding='utf-8') as f:
existing_content = f.read()
# Add large files
with open(gitignore_path, 'a', encoding='utf-8') as f:
if existing_content and not existing_content.endswith('\n'):
f.write('\n')
f.write("\n# Große Dateien (automatisch hinzugefügt)\n")
for file, _ in large_files:
f.write(f"{file}\n")
messagebox.showinfo("Erfolg",
".gitignore wurde aktualisiert.\n\n"
"Bitte committen Sie die .gitignore-Datei und versuchen Sie es erneut.")
except Exception as e:
messagebox.showerror("Fehler", f"Konnte .gitignore nicht erstellen: {str(e)}")
return
# Perform push
success, result = git_ops.push(project_path)
if success:
# Debug: Check where the repository actually is
debug_info = "Push erfolgreich!\n\n"
debug_info += f"Remote URLs:\n{remotes}\n\n"
# Extract owner from remote URL
import re
match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes)
if match:
remote_owner = match.group(1)
remote_repo = match.group(2)
debug_info += f"Push ging an: {remote_owner}/{remote_repo}\n\n"
# Try to find the repository
try:
# Check if repo exists under detected owner
repo = self.repo_manager.client.get_repository(remote_owner, remote_repo)
debug_info += f"✅ Repository gefunden!\n"
debug_info += f"URL: {repo.get('html_url', 'Unknown')}\n"
debug_info += f"Größe: {repo.get('size', 0)} bytes\n"
debug_info += f"Default Branch: {repo.get('default_branch', 'Keiner')}\n\n"
except:
debug_info += f"❌ Repository nicht unter {remote_owner}/{remote_repo} gefunden!\n\n"
# Search in all accessible locations
debug_info += "Suche Repository in allen Orten:\n"
# Check user repos
try:
user_repos = self.repo_manager.list_all_repositories()
found_in_user = any(r['name'] == remote_repo for r in user_repos)
debug_info += f"- In Benutzer-Repos: {'Gefunden' if found_in_user else 'Nicht gefunden'}\n"
except:
debug_info += "- In Benutzer-Repos: Fehler beim Suchen\n"
# Check org repos
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name:
try:
org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name)
found_in_org = any(r['name'] == remote_repo for r in org_repos)
debug_info += f"- In Organisation {self.gitea_explorer.organization_name}: {'Gefunden' if found_in_org else 'Nicht gefunden'}\n"
except:
debug_info += f"- In Organisation: Fehler beim Suchen\n"
debug_info += f"\nLog-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}"
messagebox.showinfo("Push Debug Info", debug_info)
# Refresh Gitea explorer
if hasattr(self, 'gitea_explorer'):
self.gitea_explorer.refresh_repositories()
else:
messagebox.showerror("Fehler", f"Push fehlgeschlagen: {result}")
def pull_from_gitea(self, project):
"""Pull changes from Gitea"""
git_ops = self.repo_manager.git_ops
success, result = git_ops.pull(Path(project.path))
if success:
messagebox.showinfo("Erfolg", "Änderungen erfolgreich gepullt!")
else:
messagebox.showerror("Fehler", f"Pull fehlgeschlagen: {result}")
def fetch_from_gitea(self, item):
"""Fetch from Gitea"""
if isinstance(item, Project):
# For local project
try:
# Hide console window on Windows
startupinfo = None
if os.name == 'nt': # Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# Run git fetch
result = subprocess.run(
["git", "fetch", "--all"],
cwd=item.path,
capture_output=True,
text=True,
startupinfo=startupinfo
)
if result.returncode == 0:
# Check if there are updates
status_result = subprocess.run(
["git", "status", "-uno"],
cwd=item.path,
capture_output=True,
text=True,
startupinfo=startupinfo
)
if "Your branch is behind" in status_result.stdout:
messagebox.showinfo("Fetch Complete",
"Updates available!\n\nUse Pull to get the latest changes.")
else:
messagebox.showinfo("Fetch Complete", "Already up to date.")
else:
messagebox.showerror("Fetch Error", f"Failed to fetch: {result.stderr}")
except Exception as e:
messagebox.showerror("Error", f"Failed to fetch: {e}")
else:
# For Gitea repo - just refresh the explorer
if hasattr(self, 'gitea_explorer'):
self.gitea_explorer.refresh_repositories()
messagebox.showinfo("Refresh", "Repository list updated")
def manage_branches(self, project):
"""Manage branches - Facade method (placeholder removed)"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.manage_branches(project)
else:
# Skip the placeholder, use the real implementation
return self._original_manage_branches_v2(project)
# _original_manage_branches removed - was placeholder only
def link_to_gitea(self, project):
"""Link to Gitea - Facade method (placeholder removed)"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.link_to_gitea(project)
else:
# Skip the placeholder, use the real implementation at line 2455
return self._original_link_to_gitea_v2(project)
# _original_link_to_gitea removed - was placeholder only
def clone_repository(self, repo):
"""Clone a Gitea repository"""
def do_clone():
try:
# Update status in main thread
self.root.after(0, lambda: self.update_status(f"Cloning {repo['name']}..."))
# Clone repository - use owner from repo data if available
repo_owner = repo.get('owner', {}).get('username') if repo.get('owner') else None
logger.info(f"Attempting to clone repository: {repo['name']}")
logger.info(f"Repository owner: {repo_owner}")
logger.info(f"Current user: {self.repo_manager.current_user.get('username')}")
if repo_owner and repo_owner != self.repo_manager.current_user.get('username'):
# Check if it's an organization repository and user is a member
try:
user_orgs = self.repo_manager.client.list_user_organizations()
org_names = [org.get('username', org.get('name', '')) for org in user_orgs]
logger.info(f"User organizations: {org_names}")
# Special handling for IntelSight organization
if repo_owner == 'IntelSight':
logger.info(f"Repository belongs to IntelSight organization - allowing clone")
elif repo_owner not in org_names:
# This is someone else's repo and user is not a member of the org
logger.warning(f"User is not a member of organization: {repo_owner}")
self.root.after(0, lambda: messagebox.showwarning(
"Not your repository",
f"This repository belongs to {repo_owner}.\nYou may need to fork it first."
))
return
else:
logger.info(f"User is member of organization: {repo_owner}")
except Exception as e:
logger.error(f"Error checking organization membership: {e}")
# If we can't check org membership, try to clone anyway
logger.info("Proceeding with clone despite org check failure")
# If we get here, the repo belongs to an org the user is a member of
# Use the actual owner (user or organization) for cloning
if repo_owner:
success, path = self.repo_manager.git_ops.clone_repository(repo_owner, repo['name'])
else:
success, path = self.repo_manager.clone_repository(repo['name'])
if success:
# Create CPM project in main thread
self.root.after(0, lambda: self._create_project_after_clone(repo['name'], str(path), repo_owner))
else:
# Get more detailed error information
error_msg = "Failed to clone repository.\n\n"
error_msg += f"Repository: {repo['name']}\n"
error_msg += f"Please check:\n"
error_msg += "- Internet connection\n"
error_msg += "- Git is installed\n"
error_msg += "- Repository permissions"
self.root.after(0, lambda: messagebox.showerror("Clone Error", error_msg))
except Exception as e:
import traceback
error_details = traceback.format_exc()
print(f"Clone error details:\n{error_details}")
error_msg = f"Failed to clone repository: {str(e)}\n\n"
if "get_user_info" in str(e):
error_msg += "Cannot connect to Gitea server. Please check your connection."
elif "git" in str(e).lower():
error_msg += "Git might not be installed or accessible."
self.root.after(0, lambda: messagebox.showerror("Clone Error", error_msg))
# Run in thread to avoid blocking UI
threading.Thread(target=do_clone, daemon=True).start()
def _create_project_after_clone(self, repo_name, path, repo_owner=None):
"""Create project after successful clone"""
try:
# Add project to CPM
project = self.project_manager.add_project(repo_name, path)
# Update project with Gitea repo reference if owner is provided
if repo_owner:
project.gitea_repo = f"{repo_owner}/{repo_name}"
self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo)
# Refresh the project grid with differential update
self.refresh_projects(differential=True)
# Show success message
self.update_status(f"Repository '{repo_name}' cloned and added to projects")
messagebox.showinfo("Success", f"Repository cloned to:\n{path}\n\nProject added to CPM!")
# Also refresh the Gitea explorer to update status
if hasattr(self, 'gitea_explorer'):
self.gitea_explorer.refresh_repositories()
except Exception as e:
messagebox.showerror("Error", f"Repository cloned but failed to create project: {e}")
def create_project_from_repo(self, repo):
"""Create a CPM project from cloned repository"""
local_path = Path.home() / "GiteaRepos" / repo['name']
if local_path.exists():
self.project_manager.create_project(repo['name'], str(local_path))
self.refresh_projects(differential=True)
messagebox.showinfo("Success", f"Project created for {repo['name']}")
else:
messagebox.showwarning("Not Cloned", "Repository must be cloned first")
def on_closing(self):
"""Handle window closing event"""
# Stop all running processes
self.process_manager.stop_all_processes()
self.process_tracker.stop_all()
# Destroy the window
self.root.destroy()
def gitea_operation(self, project, operation: str):
"""Handle Gitea operations from project tiles"""
if operation == "status":
self.show_git_status(project)
elif operation == "commit":
self.commit_changes(project)
elif operation == "push":
self.push_to_gitea(project)
elif operation == "pull":
self.pull_from_gitea(project)
elif operation == "fetch":
self.fetch_from_gitea(project)
elif operation == "link":
self.link_to_gitea(project)
elif operation == "branch":
self.manage_branches(project)
elif operation == "init_push":
self.init_and_push_to_gitea(project)
elif operation == "init":
self.init_git_repo(project)
elif operation == "test":
self.test_gitea_connection(project)
elif operation == "verify":
self.verify_repository_on_gitea(project)
elif operation == "lfs":
self.setup_git_lfs(project)
elif operation == "manage_large":
self.manage_large_files(project)
elif operation == "fix_repo":
self.fix_repository_issues(project)
def init_and_push_to_gitea(self, project):
"""Initialize git repo and push to Gitea - Facade method"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.init_and_push_to_gitea(project)
else:
return self._original_init_and_push_to_gitea(project)
def _original_init_and_push_to_gitea(self, project):
"""Initialize git repo and push to Gitea"""
from tkinter import simpledialog
# Ask for repository name
repo_name = simpledialog.askstring(
"Neues Repository",
f"Repository-Name für '{project.name}':",
initialvalue=project.name
)
while repo_name:
try:
# Log the creation attempt
import logging
logger = logging.getLogger(__name__)
# Always try to create in organization first if available
org_name = None
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name:
org_name = self.gitea_explorer.organization_name
# Create repository on Gitea
if org_name and (not hasattr(self, 'gitea_explorer') or self.gitea_explorer.view_mode != "user"):
# Create in organization
logger.info(f"Creating repository '{repo_name}' in organization '{org_name}'")
repo = self.repo_manager.create_repository(repo_name, auto_init=False, organization=org_name)
else:
# Create as user repository
logger.info(f"Creating repository '{repo_name}' as user repository")
repo = self.repo_manager.create_repository(repo_name, auto_init=False, organization=None)
logger.info(f"Repository created: {repo}")
# Verify repository was created
repo_owner = repo.get('owner', {}).get('username', 'Unknown')
repo_url = repo.get('html_url', 'Unknown')
# Check if repo was created in the correct place
expected_owner = org_name if org_name else self.repo_manager.client.config.username
if repo_owner != expected_owner:
messagebox.showwarning("Achtung",
f"Repository wurde unter falschem Owner erstellt!\n\n"
f"Erwartet: {expected_owner}\n"
f"Erstellt unter: {repo_owner}\n\n"
f"URL: {repo_url}")
else:
messagebox.showinfo("Repository erstellt",
f"Repository '{repo_name}' wurde erstellt.\n\n"
f"Owner: {repo_owner}\n"
f"URL: {repo_url}")
# Initialize local git repo
git_ops = self.repo_manager.git_ops
success, msg = git_ops.init_repository(Path(project.path))
if success:
# Check for large files before adding
large_files = []
import os
for root, dirs, files in os.walk(project.path):
# Skip .git directory
if '.git' in root:
continue
for file in files:
file_path = os.path.join(root, file)
try:
# Check if file is larger than 50MB (Gitea's typical limit)
file_size = os.path.getsize(file_path)
if file_size > 50 * 1024 * 1024:
size_mb = file_size / (1024 * 1024)
# Store relative path from project root
rel_path = os.path.relpath(file_path, project.path)
large_files.append((rel_path, size_mb))
except:
pass
if large_files:
msg = (f"⚠️ WARNUNG: Große Dateien gefunden!\n\n"
f"Gitea hat ein Upload-Limit. Folgende Dateien sind zu groß:\n\n")
total_size = 0
for i, (file, size_mb) in enumerate(large_files[:5]):
msg += f"{file} ({size_mb:.1f} MB)\n"
total_size += size_mb
if len(large_files) > 5:
msg += f" ... und {len(large_files) - 5} weitere\n"
msg += f"\nGesamtgröße großer Dateien: {total_size:.1f} MB\n"
msg += ("\n❌ DIESE DATEIEN KÖNNEN NICHT ZU GITEA GEPUSHT WERDEN!\n\n"
"Optionen:\n"
"1. Abbrechen und große Dateien entfernen\n"
"2. Große Dateien zur .gitignore hinzufügen\n"
"3. Git LFS einrichten (fortgeschritten)\n\n"
"Trotzdem fortfahren? (Push wird fehlschlagen!)")
if not messagebox.askyesno("Große Dateien gefunden", msg, icon='warning'):
# Help user create .gitignore
if messagebox.askyesno("Hilfe", "Soll ich eine .gitignore-Datei mit den großen Dateien erstellen?"):
gitignore_path = Path(project.path) / ".gitignore"
with open(gitignore_path, 'a', encoding='utf-8') as f:
f.write("\n# Große Dateien automatisch hinzugefügt\n")
for file, _ in large_files:
f.write(f"{file}\n")
messagebox.showinfo("Erfolg", ".gitignore wurde aktualisiert. Bitte erneut versuchen.")
return
# Add all files
git_ops.add(Path(project.path))
# Initial commit
git_ops.commit(Path(project.path), "Initial commit")
# Add remote and push (use organization if repo was created there)
# Get owner from the created repository
owner = None
if 'owner' in repo and repo['owner']:
owner = repo['owner'].get('username', repo['owner'].get('login'))
if not owner:
# Fallback to determining owner based on mode
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.view_mode == "organization":
if self.gitea_explorer.organization_name:
owner = self.gitea_explorer.organization_name
else:
owner = self.repo_manager.client.config.username
logger.info(f"Pushing to repository with owner: {owner}, repo: {repo_name}")
logger.info(f"Repository details: {repo}")
success, msg = git_ops.push_existing_repo_to_gitea(
Path(project.path),
owner,
repo_name
)
if success:
# Show push details for debugging
push_info = f"Push-Details:\n"
push_info += f"Repository: {repo_name}\n"
push_info += f"Owner: {owner}\n"
push_info += f"Branch: {current_branch} -> main\n\n"
# Get remote URL
success_remote, remotes = git_ops.remote_list(Path(project.path))
if success_remote:
push_info += f"Remote URLs:\n{remotes}\n\n"
# Verify the repository exists on Gitea
try:
verify_repo = self.repo_manager.client.get_repository(owner, repo_name)
repo_url = verify_repo.get('html_url', 'Unknown')
clone_url = verify_repo.get('clone_url', 'Unknown')
# Check if repository has content
has_content = verify_repo.get('size', 0) > 0 or verify_repo.get('default_branch')
push_info += f"Repository gefunden auf Gitea:\n"
push_info += f"URL: {repo_url}\n"
push_info += f"Clone URL: {clone_url}\n"
push_info += f"Hat Inhalt: {'Ja' if has_content else 'Nein'}\n"
push_info += f"Größe: {verify_repo.get('size', 0)} bytes\n"
push_info += f"Default Branch: {verify_repo.get('default_branch', 'Keiner')}\n\n"
if not has_content:
push_info += "⚠️ WARNUNG: Repository existiert aber scheint leer zu sein!\n\n"
push_info += "Bitte prüfen Sie:\n"
push_info += f"1. Öffnen Sie: {repo_url}\n"
push_info += f"2. Log-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}"
# Update project with Gitea repo reference
project.gitea_repo = f"{owner}/{repo_name}"
self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo)
messagebox.showinfo("Push abgeschlossen", push_info)
except Exception as e:
push_info += f"\n⚠️ Konnte Repository nicht auf Gitea verifizieren:\n{str(e)}\n\n"
push_info += "Mögliche Ursachen:\n"
push_info += "- Repository wurde unter anderem Namen/Owner erstellt\n"
push_info += "- Berechtigungsprobleme\n"
push_info += "- Netzwerkprobleme\n\n"
push_info += f"Log-Datei prüfen: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}"
messagebox.showwarning("Push Status unklar", push_info)
# Refresh Gitea explorer if exists
if hasattr(self, 'gitea_explorer'):
self.gitea_explorer.refresh_repositories()
else:
messagebox.showerror("Fehler", f"Push fehlgeschlagen: {msg}")
else:
messagebox.showerror("Fehler", f"Git-Initialisierung fehlgeschlagen: {msg}")
break
except Exception as e:
error_msg = str(e)
if "409" in error_msg:
# Repository exists already
action = messagebox.askyesno(
"Repository existiert bereits",
f"Ein Repository mit dem Namen '{repo_name}' existiert bereits.\n\n"
"Möchten Sie einen anderen Namen wählen?"
)
if action:
repo_name = simpledialog.askstring(
"Neues Repository",
f"Bitte anderen Namen wählen ('{repo_name}' existiert bereits):",
initialvalue=f"{repo_name}-2"
)
else:
break
else:
messagebox.showerror("Fehler", f"Fehler beim Erstellen des Repositories: {error_msg}")
break
def init_git_repo(self, project):
"""Initialize git repository for project"""
git_ops = self.repo_manager.git_ops
success, msg = git_ops.init_repository(Path(project.path))
if success:
messagebox.showinfo("Erfolg", "Git-Repository initialisiert!")
else:
messagebox.showerror("Fehler", f"Initialisierung fehlgeschlagen: {msg}")
def test_gitea_connection(self, project):
"""Test Gitea connection - Facade method (consolidates duplicates)"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.test_gitea_connection(project)
else:
# Use the second version (enhanced) as default
return self._original_test_gitea_connection_v2(project)
def _original_test_gitea_connection(self, project):
"""Test Gitea connection and show permissions"""
try:
# Test API connection
user_info = self.repo_manager.client.get_user_info()
info = "Gitea-Verbindung erfolgreich!\n\n"
info += f"Benutzer: {user_info.get('username', 'Unknown')}\n"
info += f"E-Mail: {user_info.get('email', 'Unknown')}\n"
info += f"Admin: {'Ja' if user_info.get('is_admin', False) else 'Nein'}\n\n"
# Check organizations
orgs = self.repo_manager.client.list_user_organizations()
if orgs:
info += "Organisationen:\n"
for org in orgs:
org_name = org.get('username', org.get('name', 'Unknown'))
info += f"{org_name}"
# Check teams/permissions in org
teams = self.repo_manager.client.get_user_teams_in_org(org_name)
if teams:
team_names = [t.get('name', 'Unknown') for t in teams]
info += f" (Teams: {', '.join(team_names)})"
info += "\n"
else:
info += "Keine Organisationen\n"
info += f"\nServer: {self.repo_manager.client.config.base_url}"
messagebox.showinfo("Verbindungstest", info)
except Exception as e:
messagebox.showerror("Verbindungsfehler", f"Konnte keine Verbindung zu Gitea herstellen:\n\n{str(e)}")
def verify_repository_on_gitea(self, project):
"""Verify repository on Gitea - Facade method (consolidates duplicates)"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.verify_repository_on_gitea(project)
else:
# Use the second version (with scrollable info) as default
return self._original_verify_repository_on_gitea_v2(project)
def _original_verify_repository_on_gitea(self, project):
"""Verify if repository exists on Gitea and show detailed info"""
import re
from pathlib import Path
project_path = Path(project.path)
# Check if it's a git repo
if not (project_path / ".git").exists():
messagebox.showinfo("Info", "Dies ist kein Git-Repository.")
return
# Get remote info
git_ops = self.repo_manager.git_ops
success, remotes = git_ops.remote_list(project_path)
if not success or not remotes:
messagebox.showinfo("Info", "Kein Remote-Repository konfiguriert.")
return
# Extract owner and repo name from remote URL
match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes)
if not match:
messagebox.showwarning("Warnung", f"Konnte Repository-Info nicht aus Remote-URL extrahieren:\n{remotes}")
return
remote_owner = match.group(1)
remote_repo = match.group(2)
info = f"Suche Repository: {remote_owner}/{remote_repo}\n\n"
# Check if repo exists under detected owner
try:
repo = self.repo_manager.client.get_repository(remote_owner, remote_repo)
info += "✅ Repository gefunden!\n\n"
info += f"Name: {repo.get('name', 'Unknown')}\n"
info += f"Owner: {repo.get('owner', {}).get('username', 'Unknown')}\n"
info += f"URL: {repo.get('html_url', 'Unknown')}\n"
info += f"Clone URL: {repo.get('clone_url', 'Unknown')}\n"
info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n"
info += f"Größe: {repo.get('size', 0) / 1024:.1f} KB\n"
info += f"Default Branch: {repo.get('default_branch', 'Keiner')}\n"
info += f"Erstellt: {repo.get('created_at', 'Unknown')}\n"
info += f"Zuletzt aktualisiert: {repo.get('updated_at', 'Unknown')}\n"
# Check for content
has_content = repo.get('size', 0) > 0 or repo.get('default_branch')
if not has_content:
info += "\n⚠️ WARNUNG: Repository scheint leer zu sein!\n"
except Exception as e:
info += f"❌ Repository nicht unter {remote_owner}/{remote_repo} gefunden!\n"
info += f"Fehler: {str(e)}\n\n"
# Search in all locations
info += "Suche in allen verfügbaren Orten:\n\n"
# Check user repos
try:
user_repos = self.repo_manager.list_all_repositories()
found_in_user = [r for r in user_repos if r['name'] == remote_repo]
if found_in_user:
info += f"✅ Gefunden in Benutzer-Repos:\n"
for r in found_in_user:
info += f" - {r.get('owner', {}).get('username', 'Unknown')}/{r['name']}\n"
else:
info += "❌ Nicht in Benutzer-Repos gefunden\n"
except Exception as e:
info += f"❌ Fehler beim Durchsuchen der Benutzer-Repos: {str(e)}\n"
# Check org repos
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name:
try:
org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name)
found_in_org = [r for r in org_repos if r['name'] == remote_repo]
if found_in_org:
info += f"✅ Gefunden in Organisation {self.gitea_explorer.organization_name}:\n"
for r in found_in_org:
info += f" - {r.get('owner', {}).get('username', 'Unknown')}/{r['name']}\n"
else:
info += f"❌ Nicht in Organisation {self.gitea_explorer.organization_name} gefunden\n"
except Exception as e:
info += f"❌ Fehler beim Durchsuchen der Organisations-Repos: {str(e)}\n"
# Check for debug file
debug_file = project_path / "gitea_push_debug.txt"
if debug_file.exists():
info += f"\n\nDebug-Datei gefunden: {debug_file}\n"
try:
with open(debug_file, 'r', encoding='utf-8') as f:
debug_content = f.read()
info += "Debug-Inhalt:\n"
info += "-" * 40 + "\n"
info += debug_content[:1000] # First 1000 chars
if len(debug_content) > 1000:
info += "\n... (gekürzt)"
except:
info += "Konnte Debug-Datei nicht lesen.\n"
info += f"\n\nLog-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}"
# Show in scrollable dialog
self._show_scrollable_info("Repository-Verifizierung", info)
def fix_repository_issues(self, project):
"""Fix repository issues - Facade method"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.fix_repository_issues(project)
else:
return self._original_fix_repository_issues(project)
def _original_fix_repository_issues(self, project):
"""Fix common repository issues"""
project_path = Path(project.path)
git_ops = self.repo_manager.git_ops
# Check current status
issues = []
# Check if it's a git repo
if not (project_path / ".git").exists():
issues.append("❌ Kein Git-Repository")
else:
# Check remote
success, remotes = git_ops.remote_list(project_path)
if success and remotes:
issues.append(f"✅ Remote vorhanden:\n{remotes}")
# Check for authentication issues
if "Authentication failed" in remotes or "21cbba8d" in remotes:
issues.append("⚠️ Alter/falscher Token im Remote gefunden")
else:
issues.append("❌ Kein Remote konfiguriert")
# Check for LFS
gitattributes = project_path / ".gitattributes"
if gitattributes.exists():
with open(gitattributes, 'r') as f:
if 'filter=lfs' in f.read():
issues.append("⚠️ Git LFS ist konfiguriert (kann Probleme verursachen)")
msg = "Repository-Status:\n\n" + "\n".join(issues) + "\n\nWas möchten Sie tun?"
# Create dialog
dialog = ctk.CTkToplevel(self.root)
dialog.title("Repository reparieren")
dialog.geometry("600x400")
dialog.transient(self.root)
# Center dialog
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 400) // 2
dialog.geometry(f"600x400+{x}+{y}")
# Message
text_widget = ctk.CTkTextbox(dialog, width=580, height=250)
text_widget.pack(padx=10, pady=10)
text_widget.insert("1.0", msg)
text_widget.configure(state="disabled")
# Buttons
button_frame = ctk.CTkFrame(dialog)
button_frame.pack(fill="x", padx=10, pady=(0, 10))
def fix_remote():
"""Fix remote URL with correct credentials"""
dialog.destroy()
# Get current organization
org_name = "IntelSight"
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name:
org_name = self.gitea_explorer.organization_name
# Remove old remote
git_ops.remote_remove(project_path, "origin")
# Add correct remote
success, msg = git_ops.add_remote_to_existing_repo(
project_path, org_name, project.name
)
if success:
messagebox.showinfo("Erfolg",
f"Remote wurde korrigiert!\n\n"
f"Repository: {org_name}/{project.name}\n"
f"Sie können jetzt pushen.")
else:
messagebox.showerror("Fehler", f"Fehler beim Korrigieren: {msg}")
def disable_lfs():
"""Disable LFS temporarily"""
dialog.destroy()
success, msg = git_ops.disable_lfs_for_push(project_path)
if success:
messagebox.showinfo("Erfolg", "Git LFS wurde deaktiviert.\n\nVersuchen Sie den Push erneut.")
else:
messagebox.showerror("Fehler", f"Fehler: {msg}")
def check_on_gitea():
"""Check if repo exists on Gitea"""
dialog.destroy()
self.verify_repository_on_gitea(project)
# Add buttons based on issues
if any("falscher Token" in issue for issue in issues):
fix_btn = ctk.CTkButton(button_frame, text="🔧 Remote korrigieren",
command=fix_remote, width=180, height=40)
fix_btn.pack(side="left", padx=5)
if any("LFS" in issue for issue in issues):
lfs_btn = ctk.CTkButton(button_frame, text="🚫 LFS deaktivieren",
command=disable_lfs, width=180, height=40)
lfs_btn.pack(side="left", padx=5)
check_btn = ctk.CTkButton(button_frame, text="🔍 Auf Gitea prüfen",
command=check_on_gitea, width=180, height=40)
check_btn.pack(side="left", padx=5)
close_btn = ctk.CTkButton(dialog, text="Schließen", command=dialog.destroy, width=100)
close_btn.pack(pady=10)
dialog.grab_set()
def manage_large_files(self, project):
"""Manage large files - Facade method"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.manage_large_files(project)
else:
return self._original_manage_large_files(project)
def _original_manage_large_files(self, project):
"""Manage large files in the repository"""
project_path = Path(project.path)
git_ops = self.repo_manager.git_ops
# Check for large files
large_files = git_ops.check_large_files(project_path, 50) # Files > 50MB
if not large_files:
messagebox.showinfo("Info", "Keine großen Dateien gefunden (>50MB).\n\nIhr Repository kann problemlos gepusht werden.")
return
# Show large files
msg = f"Gefundene große Dateien ({len(large_files)} Dateien):\n\n"
total_size = 0
for i, (file, size) in enumerate(large_files[:15]):
size_mb = size / (1024 * 1024)
msg += f"{file} ({size_mb:.1f} MB)\n"
total_size += size
if i >= 14 and len(large_files) > 15:
msg += f" ... und {len(large_files) - 15} weitere\n"
break
total_mb = total_size / (1024 * 1024)
msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n"
msg += "Was möchten Sie tun?"
# Create dialog with options
dialog = ctk.CTkToplevel(self.root)
dialog.title("Große Dateien verwalten")
dialog.geometry("600x500")
dialog.transient(self.root)
# Center dialog
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 500) // 2
dialog.geometry(f"600x500+{x}+{y}")
# Message
text_widget = ctk.CTkTextbox(dialog, width=580, height=300)
text_widget.pack(padx=10, pady=10)
text_widget.insert("1.0", msg)
text_widget.configure(state="disabled")
# Buttons
button_frame = ctk.CTkFrame(dialog)
button_frame.pack(fill="x", padx=10, pady=(0, 10))
def remove_from_git():
"""Remove large files from git but keep locally"""
dialog.destroy()
try:
# Create .gitignore
gitignore_path = project_path / ".gitignore"
existing_content = ""
if gitignore_path.exists():
with open(gitignore_path, 'r', encoding='utf-8') as f:
existing_content = f.read()
# Add large files to .gitignore
with open(gitignore_path, 'a', encoding='utf-8') as f:
if existing_content and not existing_content.endswith('\n'):
f.write('\n')
f.write("\n# Große Dateien (automatisch hinzugefügt)\n")
for file, _ in large_files:
f.write(f"{file}\n")
# Remove files from git index
removed_count = 0
failed_files = []
for file, _ in large_files:
cmd = ["git", "rm", "--cached", file]
success, _, stderr = git_ops._run_git_command(cmd, cwd=project_path)
if success:
removed_count += 1
else:
failed_files.append((file, stderr))
if removed_count > 0:
# Commit the changes
git_ops.add(Path(project_path), [".gitignore"])
success, _ = git_ops.commit(Path(project_path),
f"Große Dateien aus Git entfernt ({removed_count} Dateien)")
result_msg = f"{removed_count} Dateien wurden aus Git entfernt.\n\n"
result_msg += "Die Dateien sind weiterhin lokal vorhanden.\n"
if failed_files:
result_msg += f"\n⚠️ {len(failed_files)} Dateien konnten nicht entfernt werden.\n"
result_msg += "\nSie können jetzt pushen!"
messagebox.showinfo("Erfolg", result_msg)
else:
messagebox.showwarning("Warnung", "Keine Dateien konnten entfernt werden.")
except Exception as e:
messagebox.showerror("Fehler", f"Fehler beim Entfernen: {str(e)}")
def show_gitignore():
"""Show .gitignore content"""
dialog.destroy()
gitignore_content = "# Große Dateien\n"
for file, _ in large_files:
gitignore_content += f"{file}\n"
# Create dialog to show content
info_dialog = ctk.CTkToplevel(self.root)
info_dialog.title(".gitignore Inhalt")
info_dialog.geometry("500x400")
label = ctk.CTkLabel(info_dialog, text="Fügen Sie diese Zeilen zu .gitignore hinzu:", font=("Arial", 14))
label.pack(pady=10)
text = ctk.CTkTextbox(info_dialog, width=480, height=300)
text.pack(padx=10, pady=10)
text.insert("1.0", gitignore_content)
def copy_to_clipboard():
self.root.clipboard_clear()
self.root.clipboard_append(gitignore_content)
messagebox.showinfo("Kopiert", "Inhalt wurde in die Zwischenablage kopiert!")
copy_btn = ctk.CTkButton(info_dialog, text="In Zwischenablage kopieren", command=copy_to_clipboard)
copy_btn.pack(pady=10)
info_dialog.grab_set()
def check_status():
"""Check git status"""
dialog.destroy()
success, status = git_ops.status(project_path)
if success:
self._show_scrollable_info("Git Status", f"Git Status für {project.name}:\n\n{status}")
else:
messagebox.showerror("Fehler", "Konnte Git Status nicht abrufen.")
# Add buttons
remove_btn = ctk.CTkButton(button_frame, text="🗑️ Aus Git entfernen\n(Dateien bleiben lokal)",
command=remove_from_git, width=180, height=60)
remove_btn.pack(side="left", padx=5)
gitignore_btn = ctk.CTkButton(button_frame, text="📝 .gitignore anzeigen",
command=show_gitignore, width=180, height=60)
gitignore_btn.pack(side="left", padx=5)
status_btn = ctk.CTkButton(button_frame, text="📊 Git Status",
command=check_status, width=180, height=60)
status_btn.pack(side="left", padx=5)
# Info text
info_label = ctk.CTkLabel(dialog,
text="Tipp: 'Aus Git entfernen' ist die einfachste Lösung.\nDie Dateien bleiben auf Ihrer Festplatte erhalten!",
font=("Arial", 12), text_color="yellow")
info_label.pack(pady=10)
close_btn = ctk.CTkButton(dialog, text="Schließen", command=dialog.destroy, width=100)
close_btn.pack(pady=10)
dialog.grab_set()
def setup_git_lfs(self, project):
"""Setup Git LFS - Facade method"""
if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False):
return self._gitea_handler.setup_git_lfs(project)
else:
return self._original_setup_git_lfs(project)
def _original_setup_git_lfs(self, project):
"""Setup Git LFS for the project"""
project_path = Path(project.path)
# Check for large files
git_ops = self.repo_manager.git_ops
large_files = git_ops.check_large_files(project_path, 50) # Files > 50MB
if not large_files:
messagebox.showinfo("Info", "Keine großen Dateien gefunden (>50MB).\n\nGit LFS ist möglicherweise nicht erforderlich.")
return
# Show large files and ask what to do
msg = f"Gefundene große Dateien ({len(large_files)} Dateien):\n\n"
total_size = 0
for i, (file, size) in enumerate(large_files[:10]):
size_mb = size / (1024 * 1024)
msg += f"{file} ({size_mb:.1f} MB)\n"
total_size += size
if i >= 9 and len(large_files) > 10:
msg += f" ... und {len(large_files) - 10} weitere\n"
break
total_mb = total_size / (1024 * 1024)
msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n"
msg += "Möchten Sie Git LFS für diese Dateien einrichten?"
if messagebox.askyesno("Git LFS einrichten", msg):
# Check if Git LFS is installed
success, result = git_ops.setup_lfs(project_path)
if not success:
messagebox.showerror("Fehler", result)
return
# Ask which files to track
choices = {
"all": "Alle großen Dateien (>50MB)",
"types": "Nach Dateityp (z.B. *.mp4, *.zip)",
"specific": "Spezifische Dateien auswählen"
}
dialog = ctk.CTkToplevel(self.root)
dialog.title("LFS Tracking-Optionen")
dialog.geometry("400x300")
dialog.transient(self.root)
# Center dialog
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 400) // 2
y = (dialog.winfo_screenheight() - 300) // 2
dialog.geometry(f"400x300+{x}+{y}")
selected_option = tk.StringVar(value="all")
label = ctk.CTkLabel(dialog, text="Wie möchten Sie die Dateien tracken?", font=("Arial", 14))
label.pack(pady=20)
for key, text in choices.items():
radio = ctk.CTkRadioButton(dialog, text=text, variable=selected_option, value=key)
radio.pack(pady=5, padx=20, anchor="w")
result = {"ok": False}
def on_ok():
result["ok"] = True
result["choice"] = selected_option.get()
dialog.destroy()
ok_btn = ctk.CTkButton(dialog, text="OK", command=on_ok)
ok_btn.pack(pady=20)
dialog.grab_set()
dialog.wait_window()
if not result.get("ok"):
return
# Process based on choice
patterns = []
if result["choice"] == "all":
# Track all large files
patterns = [file for file, _ in large_files]
elif result["choice"] == "types":
# Get unique extensions
extensions = set()
for file, _ in large_files:
ext = Path(file).suffix
if ext:
extensions.add(ext)
# Ask which extensions to track
ext_msg = "Folgende Dateitypen wurden gefunden:\n"
for ext in extensions:
ext_msg += f" • *{ext}\n"
ext_msg += "\nWelche möchten Sie mit LFS tracken?\n(Komma-getrennt, z.B. .mp4,.zip)"
from tkinter import simpledialog
selected = simpledialog.askstring("Dateitypen auswählen", ext_msg)
if selected:
for ext in selected.split(","):
ext = ext.strip()
if not ext.startswith("."):
ext = "." + ext
patterns.append(f"*{ext}")
else:
# Specific files - show a selection dialog
# For simplicity, track all for now
patterns = [file for file, _ in large_files]
if patterns:
# Track with LFS
success, result = git_ops.track_with_lfs(project_path, patterns)
if success:
messagebox.showinfo("Erfolg",
f"{result}\n\n"
"Nächste Schritte:\n"
"1. Committen Sie die .gitattributes Datei\n"
"2. Große Dateien werden nun über LFS verwaltet\n"
"3. Der Push sollte jetzt funktionieren")
# Offer to migrate existing files
if messagebox.askyesno("Migration",
"Möchten Sie existierende große Dateien zu LFS migrieren?\n\n"
"Dies wird die Dateien aus dem Git-History entfernen und neu hinzufügen."):
success, result = git_ops.migrate_to_lfs(project_path, [f for f, _ in large_files])
if success:
messagebox.showinfo("Erfolg", result)
else:
messagebox.showerror("Fehler", result)
else:
messagebox.showerror("Fehler", result)
def _show_scrollable_info(self, title, content):
"""Show information in a scrollable dialog - Facade method"""
if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False):
return self._ui_handler._show_scrollable_info(title, content)
else:
return self._original_show_scrollable_info(title, content)
def _original_show_scrollable_info(self, title, content):
"""Show information in a scrollable dialog"""
dialog = ctk.CTkToplevel(self.root)
dialog.title(title)
dialog.geometry("600x500")
# Center the dialog
dialog.transient(self.root)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 500) // 2
dialog.geometry(f"600x500+{x}+{y}")
# Text widget with scrollbar
text_frame = ctk.CTkFrame(dialog)
text_frame.pack(fill="both", expand=True, padx=10, pady=10)
text_widget = ctk.CTkTextbox(text_frame, width=580, height=450)
text_widget.pack(fill="both", expand=True)
text_widget.insert("1.0", content)
text_widget.configure(state="disabled")
# Close button
close_btn = ctk.CTkButton(
dialog,
text="Schließen",
command=dialog.destroy,
width=100
)
close_btn.pack(pady=(0, 10))
dialog.grab_set()
dialog.focus_set()
def _original_manage_branches_v2(self, project):
"""Manage branches for a project - Full implementation"""
from tkinter import simpledialog
git_ops = self.repo_manager.git_ops
# Get current branches
success, branches = git_ops.branch(Path(project.path), list_all=True)
if success:
current_branch = None
branch_list = []
for line in branches.strip().split('\n'):
if line.startswith('*'):
current_branch = line[2:].strip()
branch_list.append(f"{line.strip()} (aktuell)")
else:
branch_list.append(line.strip())
# Show options
action = messagebox.askyesnocancel(
"Branch-Verwaltung",
f"Aktuelle Branches:\n{chr(10).join(branch_list)}\n\n"
"Ja = Neuen Branch erstellen\n"
"Nein = Zu anderem Branch wechseln\n"
"Abbrechen = Schließen"
)
if action is True: # Create new branch
branch_name = simpledialog.askstring(
"Neuer Branch",
"Name des neuen Branches:",
parent=self.root
)
if branch_name:
success, result = git_ops.checkout(Path(project.path), branch_name, create=True)
if success:
messagebox.showinfo("Erfolg", f"Branch '{branch_name}' erstellt und gewechselt")
else:
messagebox.showerror("Fehler", f"Branch-Erstellung fehlgeschlagen: {result}")
elif action is False: # Switch branch
branch_name = simpledialog.askstring(
"Branch wechseln",
"Zu welchem Branch wechseln?",
parent=self.root
)
if branch_name:
success, result = git_ops.checkout(Path(project.path), branch_name)
if success:
messagebox.showinfo("Erfolg", f"Zu Branch '{branch_name}' gewechselt")
else:
messagebox.showerror("Fehler", f"Branch-Wechsel fehlgeschlagen: {result}")
else:
messagebox.showerror("Fehler", f"Konnte Branches nicht abrufen: {branches}")
def _original_link_to_gitea_v2(self, project):
"""Link local project to Gitea repository - Full implementation"""
from tkinter import simpledialog
# Ask for repository name
repo_name = simpledialog.askstring(
"Mit Gitea verknüpfen",
"Name des Gitea-Repositories:",
initialvalue=project.name,
parent=self.root
)
if repo_name:
git_ops = self.repo_manager.git_ops
# Check if repo exists on Gitea
try:
repo = self.repo_manager.client.get_repository(
self.repo_manager.client.config.username,
repo_name
)
# Add remote
success, msg = git_ops.add_remote_to_existing_repo(
Path(project.path),
self.repo_manager.client.config.username,
repo_name
)
if success:
# Update project with Gitea repo reference
project.gitea_repo = f"{self.repo_manager.client.config.username}/{repo_name}"
self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo)
messagebox.showinfo("Erfolg", f"Erfolgreich mit Repository '{repo_name}' verknüpft!")
else:
messagebox.showerror("Fehler", f"Verknüpfung fehlgeschlagen: {msg}")
except Exception as e:
# Repository doesn't exist
if messagebox.askyesno("Repository erstellen?",
f"Repository '{repo_name}' existiert nicht.\nSoll es erstellt werden?"):
try:
# Create repository
repo = self.repo_manager.create_repository(repo_name, auto_init=False)
# Add remote
success, msg = git_ops.add_remote_to_existing_repo(
Path(project.path),
self.repo_manager.client.config.username,
repo_name
)
if success:
# Update project with Gitea repo reference
project.gitea_repo = f"{self.repo_manager.client.config.username}/{repo_name}"
self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo)
messagebox.showinfo("Erfolg",
f"Repository '{repo_name}' erstellt und verknüpft!")
else:
messagebox.showerror("Fehler", f"Verknüpfung fehlgeschlagen: {msg}")
except Exception as create_error:
messagebox.showerror("Fehler",
f"Repository-Erstellung fehlgeschlagen: {str(create_error)}")
def _original_test_gitea_connection_v2(self, project=None):
"""Test Gitea connection and show detailed information - Enhanced version"""
try:
# Test basic connection
user_info = self.repo_manager.client.get_user_info()
username = user_info.get('username', 'Unknown')
# Get organizations
orgs = self.repo_manager.client.list_user_organizations()
org_names = [org['username'] for org in orgs]
# Build info message
info = f"✅ Gitea Verbindung erfolgreich!\n\n"
info += f"Benutzer: {username}\n"
info += f"Server: {self.repo_manager.client.config.base_url}\n"
info += f"Organisationen: {', '.join(org_names) if org_names else 'Keine'}\n\n"
# Check organization permissions if in org mode
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.view_mode == "organization":
org_name = self.gitea_explorer.organization_name
if org_name:
teams = self.repo_manager.client.get_user_teams_in_org(org_name)
if teams:
info += f"Teams in {org_name}:\n"
for team in teams:
info += f" - {team.get('name', 'Unknown')} "
perms = []
if team.get('can_create_org_repo'):
perms.append("kann Repos erstellen")
if team.get('permission') == 'admin':
perms.append("Admin")
elif team.get('permission') == 'write':
perms.append("Schreiben")
elif team.get('permission') == 'read':
perms.append("Lesen")
info += f"({', '.join(perms) if perms else 'keine Rechte'})\n"
else:
info += f"⚠️ Keine Teams in Organisation {org_name} gefunden!\n"
info += "Dies könnte der Grund sein, warum Repositories nicht erstellt werden können.\n"
info += "\n"
# If we have a project, show its remote info
if project:
project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path'])
if project_path.exists():
success, remotes = self.repo_manager.git_ops.remote_list(project_path)
if success and remotes:
info += f"Git Remote URLs:\n{remotes}\n\n"
# Check if .git exists
if (project_path / '.git').exists():
# Get current branch
success, branch_out = self.repo_manager.git_ops._run_git_command(
["git", "branch", "--show-current"], cwd=project_path
)
if success:
info += f"Aktueller Branch: {branch_out.strip()}\n"
# List repositories in current mode
info += "Repositories:\n"
try:
if hasattr(self, 'gitea_explorer'):
if self.gitea_explorer.view_mode == "organization" and self.gitea_explorer.organization_name:
# List org repos
org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name)
info += f"In Organisation {self.gitea_explorer.organization_name}: {len(org_repos)} Repositories\n"
for repo in org_repos[:5]: # Show first 5
info += f" - {repo['name']}\n"
if len(org_repos) > 5:
info += f" ... und {len(org_repos) - 5} weitere\n"
else:
# List user repos
user_repos = self.repo_manager.list_all_repositories()
info += f"Benutzer Repositories: {len(user_repos)}\n"
for repo in user_repos[:5]: # Show first 5
info += f" - {repo['name']} (Owner: {repo.get('owner', {}).get('username', 'Unknown')})\n"
if len(user_repos) > 5:
info += f" ... und {len(user_repos) - 5} weitere\n"
except Exception as e:
info += f"Fehler beim Abrufen der Repositories: {str(e)}\n"
# Show log file location
log_file = Path.home() / ".claude_project_manager" / "gitea_operations.log"
info += f"\nLog-Datei: {log_file}"
messagebox.showinfo("Gitea Verbindungstest", info)
except Exception as e:
error_msg = f"❌ Gitea Verbindung fehlgeschlagen!\n\n"
error_msg += f"Fehler: {str(e)}\n\n"
error_msg += f"Server: {self.repo_manager.client.config.base_url}\n"
error_msg += "\nBitte prüfen Sie:\n"
error_msg += "- Netzwerkverbindung\n"
error_msg += "- API Token Gültigkeit\n"
error_msg += "- Server Erreichbarkeit"
messagebox.showerror("Gitea Verbindungstest", error_msg)
def _original_verify_repository_on_gitea_v2(self, project):
"""Verify if repository exists on Gitea and show detailed info - Enhanced version"""
project_name = project.name if hasattr(project, 'name') else project.get('name', 'Unknown')
project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path'])
info = f"Repository Verifizierung für: {project_name}\n"
info += "=" * 50 + "\n\n"
# Check git remotes
git_ops = self.repo_manager.git_ops
success, remotes = git_ops.remote_list(project_path)
if success and remotes:
info += f"Git Remote URLs:\n{remotes}\n\n"
# Extract repo name and owner from remote
import re
match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes)
if match:
remote_owner = match.group(1)
remote_repo = match.group(2)
info += f"Remote zeigt auf: {remote_owner}/{remote_repo}\n\n"
# Search for repository
info += "Suche Repository auf Gitea:\n"
# 1. Check exact location
try:
repo = self.repo_manager.client.get_repository(remote_owner, remote_repo)
info += f"✅ Gefunden unter {remote_owner}/{remote_repo}\n"
info += f" URL: {repo.get('html_url', 'Unknown')}\n"
info += f" Größe: {repo.get('size', 0)} bytes\n"
info += f" Erstellt: {repo.get('created_at', 'Unknown')}\n"
info += f" Aktualisiert: {repo.get('updated_at', 'Unknown')}\n"
info += f" Default Branch: {repo.get('default_branch', 'Unknown')}\n"
info += f" Privat: {'Ja' if repo.get('private') else 'Nein'}\n"
except Exception as e:
info += f"❌ Nicht gefunden unter {remote_owner}/{remote_repo}\n"
info += f" Fehler: {str(e)}\n"
# 2. Search in all user repos
info += "\nSuche in allen Benutzer-Repositories:\n"
try:
user_repos = self.repo_manager.list_all_repositories()
matching_repos = [r for r in user_repos if r['name'] == remote_repo or r['name'] == project_name]
if matching_repos:
for repo in matching_repos:
info += f"✅ Gefunden: {repo['owner']['username']}/{repo['name']}\n"
info += f" URL: {repo.get('html_url', 'Unknown')}\n"
else:
info += "❌ Nicht in Benutzer-Repositories gefunden\n"
except Exception as e:
info += f"❌ Fehler beim Durchsuchen: {str(e)}\n"
# 3. Search in organization
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name:
info += f"\nSuche in Organisation {self.gitea_explorer.organization_name}:\n"
try:
org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name)
matching_repos = [r for r in org_repos if r['name'] == remote_repo or r['name'] == project_name]
if matching_repos:
for repo in matching_repos:
info += f"✅ Gefunden: {repo['name']}\n"
info += f" URL: {repo.get('html_url', 'Unknown')}\n"
else:
info += "❌ Nicht in Organisation gefunden\n"
except Exception as e:
info += f"❌ Fehler beim Durchsuchen: {str(e)}\n"
else:
info += "❌ Keine Git Remote gefunden!\n"
# Check for debug file
debug_file = project_path / "gitea_push_debug.txt"
if debug_file.exists():
info += f"\n\n📄 Debug-Datei gefunden: {debug_file}\n"
try:
with open(debug_file, 'r', encoding='utf-8') as f:
debug_content = f.read()
info += "Debug-Inhalt:\n" + "-" * 30 + "\n"
info += debug_content[:1000] # First 1000 chars
if len(debug_content) > 1000:
info += "\n... (gekürzt)"
except:
info += "Konnte Debug-Datei nicht lesen\n"
# Show in scrollable dialog
dialog = ctk.CTkToplevel(self.root)
dialog.title("Repository Verifizierung")
dialog.geometry("800x600")
text_widget = ctk.CTkTextbox(dialog, wrap="word")
text_widget.pack(fill="both", expand=True, padx=10, pady=10)
text_widget.insert("1.0", info)
text_widget.configure(state="disabled")
close_btn = ctk.CTkButton(
dialog,
text="Schließen",
command=dialog.destroy
)
close_btn.pack(pady=10)
def init_activity_sync(self):
"""Initialize activity sync service"""
from services.activity_sync import activity_service
# Set callback for activity updates
activity_service.on_activities_update = self.on_activities_update
# Connect if configured
if activity_service.is_configured():
threading.Thread(target=activity_service.connect, daemon=True).start()
# Start periodic update
self.update_activity_status()
def on_activities_update(self, activities):
"""Handle activity updates from server"""
# Update UI in main thread
self.root.after(0, lambda: self.update_activity_display(activities))
def update_activity_display(self, activities):
"""Update activity display in status bar"""
count = len(activities)
if count > 0:
self.activity_label.configure(text=f"🟢 {count} Teammitglieder aktiv")
else:
self.activity_label.configure(text="")
# Update project tiles
for project_id, tile in self.project_tiles.items():
if hasattr(tile, 'check_activity'):
tile.check_activity()
def update_activity_status(self):
"""Periodic update of activity status"""
from services.activity_sync import activity_service
if activity_service.connected:
# Update display with current activities
self.update_activity_display(activity_service.activities)
# Schedule next update
self.root.after(5000, self.update_activity_status) # Update every 5 seconds
def show_activity_details(self, event=None):
"""Show detailed activity information"""
from services.activity_sync import activity_service
if not activity_service.activities:
return
# Create dialog
dialog = ctk.CTkToplevel(self.root)
dialog.title("Team-Aktivitäten")
dialog.geometry("400x300")
dialog.transient(self.root)
dialog.grab_set()
# Header
header = ctk.CTkLabel(
dialog,
text="👥 Aktive Teammitglieder",
font=FONTS['subtitle'],
text_color=COLORS['text_primary']
)
header.pack(pady=(20, 10))
# Activity list
list_frame = ctk.CTkScrollableFrame(
dialog,
fg_color=COLORS['bg_secondary']
)
list_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20))
# Display each activity
for activity in activity_service.activities:
if activity.get('isActive'):
activity_frame = ctk.CTkFrame(
list_frame,
fg_color=COLORS['bg_tile']
)
activity_frame.pack(fill="x", pady=5, padx=10)
# User and project info
info_text = f"{activity['userName']}\n📁 {activity['projectName']}"
info_label = ctk.CTkLabel(
activity_frame,
text=info_text,
font=FONTS['body'],
text_color=COLORS['text_primary'],
justify="left"
)
info_label.pack(anchor="w", padx=10, pady=5)
# Close button
close_btn = ctk.CTkButton(
dialog,
text="Schließen",
command=dialog.destroy,
width=100
)
close_btn.pack(pady=(0, 20))
# Center dialog
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - dialog.winfo_width()) // 2
y = (dialog.winfo_screenheight() - dialog.winfo_height()) // 2
dialog.geometry(f"+{x}+{y}")
def start_activity(self, project):
"""Start activity for a project"""
from services.activity_sync import activity_service
if not activity_service.is_configured():
messagebox.showinfo(
"Activity Server",
"Bitte konfigurieren Sie den Activity Server in den Einstellungen."
)
return
if not activity_service.connected:
messagebox.showwarning(
"Nicht verbunden",
"Keine Verbindung zum Activity Server."
)
return
# Start activity
success = activity_service.start_activity(
project_name=project.name,
project_path=project.path
)
if success:
self.update_status(f"Aktivität gestartet: {project.name}")
# Update tile to show activity
if project.id in self.project_tiles:
self.project_tiles[project.id].update_activity_status(True)
else:
messagebox.showerror(
"Fehler",
"Konnte Aktivität nicht starten."
)
def stop_activity(self):
"""Stop current activity"""
from services.activity_sync import activity_service
success = activity_service.stop_activity()
if success:
self.update_status("Aktivität beendet")
# Update all tiles
for tile in self.project_tiles.values():
if hasattr(tile, 'update_activity_status'):
tile.update_activity_status(False)
def _setup_exception_handler(self):
"""Setup global exception handler to log all errors"""
import sys
import tkinter
def handle_exception(exc_type, exc_value, exc_traceback):
"""Handle uncaught exceptions"""
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
# Show error dialog
error_msg = f"Ein unerwarteter Fehler ist aufgetreten:\n\n{exc_type.__name__}: {exc_value}\n\nDetails wurden im Log gespeichert."
if hasattr(self, 'root'):
messagebox.showerror("Kritischer Fehler", error_msg)
# Set the exception handler
sys.excepthook = handle_exception
# Also handle Tkinter exceptions
def handle_tk_error(self, exc, val, tb):
logger.critical(f"Tkinter exception: {exc}: {val}", exc_info=(exc, val, tb))
return True # Prevent default error dialog
tkinter.Tk.report_callback_exception = handle_tk_error
def run(self):
"""Run the application"""
self.root.mainloop()