3352 Zeilen
147 KiB
Python
3352 Zeilen
147 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
|
|
|
|
# Check service status immediately on start
|
|
self.check_service_status()
|
|
|
|
# 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)
|
|
|
|
# Own activity indicator (yellow LED)
|
|
self.own_activity_frame = ctk.CTkFrame(self.status_bar, fg_color="transparent")
|
|
self.own_activity_frame.pack(side="left", padx=(20, 0))
|
|
|
|
self.own_activity_led = ctk.CTkLabel(
|
|
self.own_activity_frame,
|
|
text="", # Will be set to yellow circle when active
|
|
font=('Segoe UI', 12),
|
|
text_color=COLORS['accent_warning'],
|
|
cursor="hand2"
|
|
)
|
|
self.own_activity_led.pack(side="left", padx=(0, 5))
|
|
|
|
self.own_activity_label = ctk.CTkLabel(
|
|
self.own_activity_frame,
|
|
text="",
|
|
font=FONTS['small'],
|
|
text_color=COLORS['accent_warning'],
|
|
cursor="hand2"
|
|
)
|
|
self.own_activity_label.pack(side="left")
|
|
|
|
# Bind mouseover events for own activity
|
|
self.own_activity_led.bind("<Enter>", self._show_own_activity_tooltip)
|
|
self.own_activity_led.bind("<Leave>", self._hide_activity_tooltip)
|
|
self.own_activity_label.bind("<Enter>", self._show_own_activity_tooltip)
|
|
self.own_activity_label.bind("<Leave>", self._hide_activity_tooltip)
|
|
self.own_activity_label.bind("<Button-1>", self.show_activity_details)
|
|
|
|
# Team activity status (blue LED)
|
|
self.activity_frame = ctk.CTkFrame(self.status_bar, fg_color="transparent")
|
|
self.activity_frame.pack(side="left", padx=(10, 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 (only local projects)
|
|
vps_ids = ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent", "activity-server-permanent"]
|
|
project_count = len([p for p in projects if p.id not in vps_ids])
|
|
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 Activity Server tile fourth
|
|
activity_project = next((p for p in projects if p.id == "activity-server-permanent"), None)
|
|
if activity_project:
|
|
if tiles_in_current_row >= tiles_per_row:
|
|
create_new_row()
|
|
self.create_project_tile_flow(activity_project, current_row_frame, is_vps=True)
|
|
tiles_in_current_row += 1
|
|
|
|
# Add separator line between VPS tiles and local projects
|
|
separator_frame = ctk.CTkFrame(self.flow_frame, fg_color="transparent")
|
|
separator_frame.pack(fill="x", pady=15)
|
|
|
|
# Use a more visible separator
|
|
separator_line = ctk.CTkFrame(
|
|
separator_frame,
|
|
height=2, # Thicker line
|
|
fg_color=COLORS['accent_secondary'] # More visible blue-gray color
|
|
)
|
|
separator_line.pack(fill="x", padx=20)
|
|
|
|
# Label for local projects section
|
|
local_label = ctk.CTkLabel(
|
|
self.flow_frame,
|
|
text="Lokale Projekte",
|
|
font=FONTS['tile_text'],
|
|
text_color=COLORS['text_secondary']
|
|
)
|
|
local_label.pack(pady=(0, 10))
|
|
|
|
# Start new row for local projects
|
|
create_new_row()
|
|
|
|
# Add local project tiles
|
|
for project in projects:
|
|
if project.id not in vps_ids:
|
|
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()
|
|
elif project.id == "activity-server-permanent":
|
|
# Handle Activity Server
|
|
self.open_activity_server()
|
|
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_activity_server(self):
|
|
"""Open Activity Server connection"""
|
|
# Check if already running
|
|
if self.process_tracker.is_running("activity-server-permanent"):
|
|
self.update_status("Activity Server connection already active", error=True)
|
|
return
|
|
|
|
# Create connection script for Activity Server
|
|
script = self.vps_connection.create_activity_server_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", "CPM Activity Server",
|
|
"--", "cmd", "/c", script_path
|
|
])
|
|
else:
|
|
process = subprocess.Popen(['cmd', '/c', 'start', 'CPM Activity Server', script_path])
|
|
|
|
# Track the process
|
|
self.process_manager.processes["activity-server-permanent"] = process
|
|
self.process_manager.save_process_data()
|
|
self.process_tracker.set_running("activity-server-permanent")
|
|
|
|
# Update tile status immediately
|
|
if "activity-server-permanent" in self.project_tiles:
|
|
self.project_tiles["activity-server-permanent"].update_status(True)
|
|
|
|
self.update_status("Connected to Activity Server")
|
|
|
|
# Update Activity Server project
|
|
activity_project = self.project_manager.get_project("activity-server-permanent")
|
|
if activity_project:
|
|
activity_project.update_last_accessed()
|
|
self.project_manager.save_projects()
|
|
|
|
# Monitor the process
|
|
self.monitor_process("activity-server-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)
|
|
|
|
# Also check service status
|
|
self.check_service_status()
|
|
|
|
def check_service_status(self):
|
|
"""Check status of Activity Server and Admin Panel"""
|
|
import threading
|
|
|
|
def check_services():
|
|
# Check Activity Server
|
|
activity_status = self.check_activity_server_status()
|
|
if "activity-server-permanent" in self.project_tiles:
|
|
self.root.after(0, lambda: self.project_tiles["activity-server-permanent"].update_service_status(activity_status))
|
|
|
|
# Check Admin Panel
|
|
admin_status = self.check_admin_panel_status()
|
|
if "admin-panel-permanent" in self.project_tiles:
|
|
self.root.after(0, lambda: self.project_tiles["admin-panel-permanent"].update_service_status(admin_status))
|
|
|
|
# Run in background thread
|
|
threading.Thread(target=check_services, daemon=True).start()
|
|
|
|
# Schedule next check in 60 seconds
|
|
self.root.after(60000, self.check_service_status)
|
|
|
|
def check_activity_server_status(self) -> bool:
|
|
"""Check if Activity Server is running"""
|
|
import requests
|
|
try:
|
|
# Activity Server runs on port 3001
|
|
# Check /api/activities endpoint - returns 401 when running (needs auth)
|
|
response = requests.get("http://91.99.192.14:3001/api/activities", timeout=5)
|
|
# 401 means server is running but needs authentication
|
|
return response.status_code in [200, 401, 403]
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
def check_admin_panel_status(self) -> bool:
|
|
"""Check if Admin Panel is accessible"""
|
|
import requests
|
|
try:
|
|
# Admin Panel runs on port 80 (not 8082)
|
|
response = requests.get("http://91.99.192.14:80/", timeout=5)
|
|
return response.status_code in [200, 301, 302] # OK or redirects
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
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"""
|
|
from services.activity_sync import activity_service
|
|
|
|
# Update own activity indicator (yellow LED)
|
|
own_activities = activity_service.get_all_current_activities()
|
|
if own_activities:
|
|
self.own_activity_led.configure(text="🟡")
|
|
activity_count = len(own_activities)
|
|
if activity_count == 1:
|
|
self.own_activity_label.configure(text=f"{own_activities[0]['projectName']}")
|
|
else:
|
|
self.own_activity_label.configure(text=f"{activity_count} Projekte aktiv")
|
|
else:
|
|
self.own_activity_led.configure(text="")
|
|
self.own_activity_label.configure(text="")
|
|
|
|
# Update team activity indicator (blue LED)
|
|
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
|
|
from utils.logger import logger
|
|
|
|
logger.debug("Periodic activity status update triggered")
|
|
|
|
if activity_service.connected:
|
|
# Update display with current activities
|
|
logger.debug(f"Updating activity display with {len(activity_service.activities)} activities")
|
|
self.update_activity_display(activity_service.activities)
|
|
else:
|
|
logger.debug("Activity service not connected, skipping update")
|
|
|
|
# Schedule next update
|
|
self.root.after(5000, self.update_activity_status) # Update every 5 seconds
|
|
|
|
def _show_own_activity_tooltip(self, event=None):
|
|
"""Show tooltip with own active projects"""
|
|
if hasattr(self, 'activity_tooltip'):
|
|
self.activity_tooltip.destroy()
|
|
|
|
from services.activity_sync import activity_service
|
|
own_activities = activity_service.get_all_current_activities()
|
|
|
|
if not own_activities:
|
|
return
|
|
|
|
self.activity_tooltip = ctk.CTkToplevel(self.root)
|
|
self.activity_tooltip.wm_overrideredirect(True)
|
|
self.activity_tooltip.configure(fg_color=COLORS['bg_secondary'])
|
|
|
|
# Build tooltip text
|
|
if len(own_activities) == 1:
|
|
text = f"🟡 {activity_service.user_name}\nAktiv: {own_activities[0]['projectName']}"
|
|
else:
|
|
text = f"🟡 {activity_service.user_name}\nAktive Projekte:"
|
|
for activity in own_activities:
|
|
text += f"\n• {activity['projectName']}"
|
|
|
|
label = ctk.CTkLabel(
|
|
self.activity_tooltip,
|
|
text=text,
|
|
font=FONTS['small'],
|
|
text_color=COLORS['text_primary'],
|
|
fg_color=COLORS['bg_secondary'],
|
|
justify="left"
|
|
)
|
|
label.pack(padx=10, pady=8)
|
|
|
|
# Position tooltip above the LED
|
|
x = self.own_activity_led.winfo_rootx()
|
|
y = self.own_activity_led.winfo_rooty() - 10
|
|
|
|
# Update tooltip position
|
|
self.activity_tooltip.update_idletasks()
|
|
tooltip_height = self.activity_tooltip.winfo_reqheight()
|
|
y = y - tooltip_height
|
|
|
|
self.activity_tooltip.geometry(f"+{x}+{y}")
|
|
|
|
def _hide_activity_tooltip(self, event=None):
|
|
"""Hide activity tooltip"""
|
|
if hasattr(self, 'activity_tooltip'):
|
|
self.activity_tooltip.destroy()
|
|
|
|
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() |