diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8fb51a3..50f8a8a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,8 @@ "Bash(build.bat)", "Bash(find:*)", "Bash(pip3 list:*)", - "Bash(curl:*)" + "Bash(curl:*)", + "Bash(jq:*)" ], "deny": [] } diff --git a/CLAUDE_PROJECT_README.md b/CLAUDE_PROJECT_README.md index 4a767fc..9dc7dfb 100644 --- a/CLAUDE_PROJECT_README.md +++ b/CLAUDE_PROJECT_README.md @@ -5,9 +5,9 @@ ## Project Overview - **Path**: `C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main` -- **Files**: 81 files -- **Size**: 1.8 MB -- **Last Modified**: 2025-07-09 22:31 +- **Files**: 84 files +- **Size**: 4.4 MB +- **Last Modified**: 2025-07-10 13:52 ## Technology Stack @@ -61,7 +61,8 @@ logs/ │ ├── cpm_20250709_215939.log │ ├── cpm_20250709_220833.log │ ├── cpm_20250709_222800.log -│ └── cpm_20250709_222952.log +│ ├── cpm_20250709_222952.log +│ └── cpm_20250709_223933.log scripts/ │ ├── check_lfs_status.bat │ ├── fix_large_files.bat @@ -168,3 +169,4 @@ This project is managed with Claude Project Manager. To work with this project: - README updated on 2025-07-09 21:31:18 - README updated on 2025-07-09 22:12:45 - README updated on 2025-07-09 22:31:20 +- README updated on 2025-07-10 13:52:48 diff --git a/data/projects.json b/data/projects.json index 2a5de90..8024916 100644 --- a/data/projects.json +++ b/data/projects.json @@ -5,7 +5,7 @@ "name": "VPS Server", "path": "claude-dev@91.99.192.14", "created_at": "2025-07-01T20:14:48.308074", - "last_accessed": "2025-07-09T22:15:03.835828", + "last_accessed": "2025-07-10T13:54:21.595494", "readme_path": "claude-dev@91.99.192.14\\CLAUDE_PROJECT_README.md", "description": "Remote VPS Server with Claude", "tags": [ @@ -51,7 +51,7 @@ "name": "ClaudeProjectManager", "path": "C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main", "created_at": "2025-07-07T21:38:23.820122", - "last_accessed": "2025-07-09T22:31:20.779365", + "last_accessed": "2025-07-10T13:52:48.621815", "readme_path": "C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main\\CLAUDE_PROJECT_README.md", "description": "", "tags": [], @@ -84,5 +84,5 @@ "gitea_repo": null } ], - "last_updated": "2025-07-09T22:31:20.779365" + "last_updated": "2025-07-10T13:54:21.595494" } \ No newline at end of file diff --git a/data/vps_readme/VPS_README.md b/data/vps_readme/VPS_README.md index 1caeda3..1c34689 100644 --- a/data/vps_readme/VPS_README.md +++ b/data/vps_readme/VPS_README.md @@ -60,4 +60,4 @@ This VPS server provides: ## Connection Log -- README generated on 2025-07-09 22:15:03 +- README generated on 2025-07-10 13:54:21 diff --git a/gui/main_window.py b/gui/main_window.py index 62b901f..6edd438 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -8,6 +8,7 @@ from tkinter import filedialog, messagebox import os import threading import subprocess +import time from typing import Optional from PIL import Image @@ -59,6 +60,14 @@ class MainWindow: self.user_interacting = False self.pending_updates = [] + # Performance optimization: Activity tracking + self._last_activities_hash = None + self._last_display_state = {} + self._verbose_logging = False # Set to True to enable debug logging + self._activity_update_interval = 5000 # Default 5 seconds + self._idle_update_interval = 10000 # 10 seconds when idle + self._last_user_interaction = time.time() + # Create main window self.root = ctk.CTk() self.root.title(WINDOW_CONFIG['title']) @@ -354,74 +363,119 @@ class MainWindow: self._differential_update(projects) return - # Full refresh - clear and rebuild - for widget in self.flow_frame.winfo_children(): - widget.destroy() - self.project_tiles.clear() + # Start async refresh + self._async_refresh_projects(projects) - # Calculate how many tiles can fit in a row + def _async_refresh_projects(self, projects): + """Asynchronously refresh projects to avoid UI freeze""" + # Clear existing widgets in background + def clear_widgets(): + for widget in self.flow_frame.winfo_children(): + widget.destroy() + self.project_tiles.clear() + + # Continue with creating tiles + self._create_tiles_batch(projects, 0) + + # Schedule widget clearing + self.root.after(1, clear_widgets) + + def _create_tiles_batch(self, projects, start_index, batch_size=3): + """Create project tiles in batches to keep UI responsive""" + if start_index == 0: + # Initialize on first batch + self._init_tile_creation(projects) + + vps_ids = ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent", "activity-server-permanent"] + + # Process VPS tiles first if we're at the beginning + if start_index == 0: + # Create VPS tiles + vps_tiles = [] + for vps_id in vps_ids: + vps_project = next((p for p in projects if p.id == vps_id), None) + if vps_project: + vps_tiles.append(vps_project) + + # Create VPS tiles + for vps_project in vps_tiles: + self._add_tile_to_flow(vps_project, is_vps=True) + + # Add separator after VPS tiles + if vps_tiles: + self._add_separator() + + # Schedule local projects creation + self.root.after(10, lambda: self._create_tiles_batch(projects, 1, batch_size)) + return + + # Get local projects only + local_projects = [p for p in projects if p.id not in vps_ids] + + # Calculate batch range + batch_start = start_index - 1 # Adjust for VPS tiles being handled separately + batch_end = min(batch_start + batch_size, len(local_projects)) + + # Create tiles for this batch + for i in range(batch_start, batch_end): + self._add_tile_to_flow(local_projects[i], is_vps=False) + + # Check if we have more projects to process + if batch_end < len(local_projects): + # Schedule next batch + self.root.after(10, lambda: self._create_tiles_batch(projects, batch_end + 1, batch_size)) + else: + # All projects processed, add the "Add Project" tile + self._add_tile_to_flow(None, is_add_tile=True) + self.update_status("Projects refreshed") + + def _init_tile_creation(self, projects): + """Initialize tile creation state""" + # Calculate tiles per 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)) + self._tiles_per_row = max(1, window_width // (tile_width + tile_margin * 2)) - # Create row frames - current_row_frame = None - tiles_in_current_row = 0 + # Initialize row tracking + self._current_row_frame = None + self._tiles_in_current_row = 0 + + def _create_new_row(self): + """Create a new row frame""" + self._current_row_frame = ctk.CTkFrame(self.flow_frame, fg_color="transparent") + self._current_row_frame.pack(fill="x", pady=5) + self._tiles_in_current_row = 0 + + def _add_tile_to_flow(self, project, is_vps=False, is_add_tile=False): + """Add a single tile to the flow layout""" + # Check if we need a new row + if self._current_row_frame is None or self._tiles_in_current_row >= self._tiles_per_row: + self._create_new_row() - # 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 + if is_add_tile: + # Create add project tile + self.create_add_tile_flow(self._current_row_frame) + else: + # Create project tile + self.create_project_tile_flow(project, self._current_row_frame, is_vps=is_vps) - # 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 + self._tiles_in_current_row += 1 + + def _add_separator(self): + """Add separator between VPS and local projects""" + # Add separator line 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_frame, + height=2, + fg_color=COLORS['accent_secondary'] ) separator_line.pack(fill="x", padx=20) - # Label for local projects section + # Label for local projects local_label = ctk.CTkLabel( self.flow_frame, text="Lokale Projekte", @@ -430,23 +484,9 @@ class MainWindow: ) 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") + # Reset row for local projects + self._current_row_frame = None + self._tiles_in_current_row = 0 def create_project_tile(self, project: Project, row: int, col: int, is_vps: bool = False): """Create a project tile (legacy grid method)""" @@ -1216,10 +1256,14 @@ class MainWindow: self.root.bind_all("<>", self._on_dropdown_select) self.root.bind_all("", self._on_focus_in) self.root.bind_all("", self._on_focus_out) + self.root.bind_all("", self._on_key_press) + self.root.bind_all("", self._on_mouse_motion) + self._last_mouse_position = (0, 0) def _on_click_start(self, event): """Track start of user interaction""" self.user_interacting = True + self._last_user_interaction = time.time() def _on_click_end(self, event): """Track end of user interaction""" @@ -1229,16 +1273,32 @@ class MainWindow: def _on_dropdown_select(self, event): """Handle dropdown selection""" self.user_interacting = True + self._last_user_interaction = time.time() 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 + self._last_user_interaction = time.time() def _on_focus_out(self, event): """Track focus loss""" self.root.after(200, self._check_pending_updates) + + def _on_key_press(self, event): + """Track keyboard activity""" + self._last_user_interaction = time.time() + + def _on_mouse_motion(self, event): + """Track mouse movement""" + # Only update if mouse has moved significantly to avoid constant updates + if hasattr(self, '_last_mouse_position'): + dx = abs(event.x_root - self._last_mouse_position[0]) + dy = abs(event.y_root - self._last_mouse_position[1]) + if dx > 10 or dy > 10: # Significant movement + self._last_user_interaction = time.time() + self._last_mouse_position = (event.x_root, event.y_root) def _check_pending_updates(self): """Process pending updates after interaction ends""" @@ -3115,18 +3175,21 @@ class MainWindow: """Update activity display in status bar""" from services.activity_sync import activity_service + # Cache current state to avoid unnecessary UI updates + if not hasattr(self, '_last_display_state'): + self._last_display_state = {'own_led': '', 'own_label': '', 'team_label': ''} + + new_state = {'own_led': '', 'own_label': '', 'team_label': ''} + # Update own activity indicator (yellow LED) own_activities = activity_service.get_all_current_activities() if own_activities: - self.own_activity_led.configure(text="🟡") + new_state['own_led'] = "🟡" activity_count = len(own_activities) if activity_count == 1: - self.own_activity_label.configure(text=f"{own_activities[0]['projectName']}") + new_state['own_label'] = 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="") + new_state['own_label'] = f"{activity_count} Projekte aktiv" # Update team activity indicator (blue LED) - count unique users, not activities unique_users = set() @@ -3138,11 +3201,20 @@ class MainWindow: count = len(unique_users) if count > 0: - self.activity_label.configure(text=f"🔵 {count} Teammitglieder aktiv") - else: - self.activity_label.configure(text="") - - # Update project tiles + new_state['team_label'] = f"🔵 {count} Teammitglieder aktiv" + + # Only update UI elements that have changed + if new_state['own_led'] != self._last_display_state.get('own_led', ''): + self.own_activity_led.configure(text=new_state['own_led']) + if new_state['own_label'] != self._last_display_state.get('own_label', ''): + self.own_activity_label.configure(text=new_state['own_label']) + if new_state['team_label'] != self._last_display_state.get('team_label', ''): + self.activity_label.configure(text=new_state['team_label']) + + self._last_display_state = new_state + + # Update project tiles only if activities have changed + # The tiles themselves will handle their own change detection for project_id, tile in self.project_tiles.items(): if hasattr(tile, 'check_activity'): tile.check_activity() @@ -3152,17 +3224,47 @@ class MainWindow: from services.activity_sync import activity_service from utils.logger import logger - logger.debug("Periodic activity status update triggered") + # Only log debug messages in verbose mode to reduce spam + if hasattr(self, '_verbose_logging') and self._verbose_logging: + 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") + # Check if activities have actually changed + current_hash = self._compute_activities_hash(activity_service.activities) + + # Only update if activities have changed + if not hasattr(self, '_last_activities_hash') or self._last_activities_hash != current_hash: + self._last_activities_hash = current_hash + if hasattr(self, '_verbose_logging') and self._verbose_logging: + logger.debug(f"Activities changed, updating display with {len(activity_service.activities)} activities") + self.update_activity_display(activity_service.activities) + + # Adaptive update interval based on user activity + current_time = time.time() + time_since_interaction = current_time - self._last_user_interaction + + # Use shorter interval if user recently interacted, longer if idle + if time_since_interaction < 60: # Active in last minute + interval = self._activity_update_interval # 5 seconds + else: # Idle for more than a minute + interval = self._idle_update_interval # 10 seconds # Schedule next update - self.root.after(5000, self.update_activity_status) # Update every 5 seconds + self.root.after(interval, self.update_activity_status) + + def _compute_activities_hash(self, activities): + """Compute a hash of activities to detect changes""" + # Create a sorted tuple of relevant activity data + activity_data = [] + for activity in activities: + activity_data.append(( + activity.get('userId'), + activity.get('projectName'), + activity.get('isActive', False) + )) + # Sort to ensure consistent ordering + activity_data.sort() + return hash(tuple(activity_data)) def _show_own_activity_tooltip(self, event=None): """Show tooltip with own active projects""" @@ -3369,6 +3471,19 @@ class MainWindow: tkinter.Tk.report_callback_exception = handle_tk_error + def set_verbose_logging(self, enabled: bool): + """Enable or disable verbose activity logging""" + self._verbose_logging = enabled + from utils.logger import logger + logger.info(f"Verbose activity logging: {'enabled' if enabled else 'disabled'}") + + def set_activity_update_intervals(self, active_interval: int = 5000, idle_interval: int = 10000): + """Set the update intervals for activity checking""" + self._activity_update_interval = active_interval + self._idle_update_interval = idle_interval + from utils.logger import logger + logger.info(f"Activity update intervals set - Active: {active_interval}ms, Idle: {idle_interval}ms") + def run(self): """Run the application""" self.root.mainloop() \ No newline at end of file diff --git a/gui/project_tile.py b/gui/project_tile.py index b755d09..a39adf1 100644 --- a/gui/project_tile.py +++ b/gui/project_tile.py @@ -879,13 +879,13 @@ pause """Check if this project has active users""" from services.activity_sync import activity_service - logger.debug(f"check_activity called for project: {self.project.name}") + # Cache the previous state to avoid unnecessary updates + if not hasattr(self, '_last_activity_state'): + self._last_activity_state = {'active': False, 'users': '', 'is_own': False} # First check if this is our own current activity is_own_current = activity_service.is_project_active_for_user(self.project.name) - logger.debug(f"Current activity check - is_own_current: {is_own_current}") - # Get all activities for this project from server active_users = [] is_own_activity = False @@ -908,25 +908,30 @@ pause else: has_other_users = True - logger.debug(f"Server activities - active_users: {active_users}, is_own_activity: {is_own_activity}, has_other_users: {has_other_users}") - # If we have a local current activity, ensure it's included if is_own_current: is_own_activity = True if activity_service.user_name not in active_users: active_users.append(activity_service.user_name) - logger.debug(f"Added local user to active_users: {activity_service.user_name}") - if active_users: - # Show indicator with all active users - user_text = ", ".join(active_users) - # If both own and others are active, show as others (orange) to indicate collaboration - final_is_own = is_own_activity and not has_other_users - logger.info(f"Updating activity status for {self.project.name}: active=True, users={user_text}, is_own={final_is_own}") - self.update_activity_status(True, user_text, final_is_own) - else: - logger.info(f"Updating activity status for {self.project.name}: active=False") - self.update_activity_status(False) + # Determine new state + new_state = { + 'active': bool(active_users), + 'users': ', '.join(active_users) if active_users else '', + 'is_own': is_own_activity and not has_other_users if active_users else False + } + + # Only update UI if state has changed + if new_state != self._last_activity_state: + self._last_activity_state = new_state + + if new_state['active']: + # Only log significant changes (not every periodic check) + logger.info(f"Activity status changed for {self.project.name}: active=True, users={new_state['users']}, is_own={new_state['is_own']}") + self.update_activity_status(True, new_state['users'], new_state['is_own']) + else: + logger.info(f"Activity status changed for {self.project.name}: active=False") + self.update_activity_status(False) def _start_activity_animation(self): """Start animated border for team activity"""