Update changes
Dieser Commit ist enthalten in:
@ -20,7 +20,8 @@
|
||||
"Bash(build.bat)",
|
||||
"Bash(find:*)",
|
||||
"Bash(pip3 list:*)",
|
||||
"Bash(curl:*)"
|
||||
"Bash(curl:*)",
|
||||
"Bash(jq:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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("<<ComboboxSelected>>", self._on_dropdown_select)
|
||||
self.root.bind_all("<FocusIn>", self._on_focus_in)
|
||||
self.root.bind_all("<FocusOut>", self._on_focus_out)
|
||||
self.root.bind_all("<Key>", self._on_key_press)
|
||||
self.root.bind_all("<Motion>", 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()
|
||||
@ -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"""
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren