Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-09 22:10:42 +02:00
Commit 4dab418f2f
73 geänderte Dateien mit 16938 neuen und 0 gelöschten Zeilen

27
.claude/settings.local.json Normale Datei
Datei anzeigen

@ -0,0 +1,27 @@
{
"permissions": {
"allow": [
"Bash(python:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(pip3 install:*)",
"Bash(ls:*)",
"Bash(sed:*)",
"Bash(cmd.exe:*)",
"Bash(./build.bat)",
"Bash(rm:*)",
"Bash(convert:*)",
"Bash(awk:*)",
"Bash(git checkout:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(mkdir:*)",
"Bash(chmod:*)",
"Bash(build.bat)",
"Bash(find:*)",
"Bash(pip3 list:*)",
"Bash(curl:*)"
],
"deny": []
}
}

124
.gitignore vendored Normale Datei
Datei anzeigen

@ -0,0 +1,124 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# IDE
.vscode/
.idea/
# Project specific
data/running_processes.json
logs/
nul
*.log
# Temporary files
*.tmp
*.bak
*~
# OS specific
.DS_Store
Thumbs.db
desktop.ini

36
ACTIVITY_SERVER_INFO.md Normale Datei
Datei anzeigen

@ -0,0 +1,36 @@
# Activity Server Information
## Problem
Der Activity Server unter `91.99.192.14:3001` ist nicht erreichbar.
## Lösungsoptionen
### Option 1: Lokalen Activity Server verwenden
Sie können einen lokalen Activity Server starten:
1. **Server URL in den Einstellungen ändern auf:**
- `http://localhost:3001`
2. **Eigenen Activity Server starten:**
- Den Activity Server lokal installieren und starten
- Oder einen Docker Container verwenden
### Option 2: Ohne Activity Server arbeiten
Die Aktivitätsfunktionen sind optional. Sie können CPM vollständig ohne Activity Server nutzen:
- Die Aktivitäts-Buttons zeigen dann eine Meldung an
- Alle anderen Funktionen arbeiten normal
### Option 3: Alternativen Server verwenden
Falls Sie Zugang zu einem anderen Activity Server haben:
1. Öffnen Sie die Einstellungen (⚙️)
2. Ändern Sie die Server URL
3. Geben Sie ggf. API Key und Benutzername ein
4. Testen Sie die Verbindung
## Verbindungstest
In den Einstellungen können Sie die Verbindung testen:
- Grün ✅ = Verbindung erfolgreich
- Rot ❌ = Verbindung fehlgeschlagen
## Hinweis
Der Activity Server ist für Team-Kollaboration gedacht und zeigt an, wer gerade an welchem Projekt arbeitet. Für Einzelnutzer ist diese Funktion nicht zwingend erforderlich.

161
CLAUDE_PROJECT_README.md Normale Datei
Datei anzeigen

@ -0,0 +1,161 @@
# ClaudeProjectManager
*This README was automatically generated by Claude Project Manager*
## Project Overview
- **Path**: `C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main`
- **Files**: 74 files
- **Size**: 663.5 KB
- **Last Modified**: 2025-07-09 21:31
## Technology Stack
### Languages
- Batch
- PowerShell
- Python
- Shell
## Project Structure
```
activity_server_cmd_fix.md
activity_server_connect.ps1
ACTIVITY_SERVER_INFO.md
app-icon.ico
app-icon.svg
build.bat
build_exe.py
CLAUDE_PROJECT_README.md
clone_fix.py
data/
│ ├── projects.json
│ ├── running_processes.json
│ ├── settings.json
│ └── vps_readme/
│ └── VPS_README.md
gui/
│ ├── config.py
│ ├── gitea_explorer.py
│ ├── gitea_toolbar.py
│ ├── main_window.py
│ ├── progress_bar.py
│ ├── project_tile.py
│ ├── settings_dialog.py
│ ├── sidebar_view.py
│ ├── styles.py
│ └── handlers/
│ ├── base_handler.py
│ ├── gitea_operations.py
│ ├── process_manager.py
│ ├── project_manager.py
│ ├── ui_helpers.py
│ └── __init__.py
logs/
│ ├── cpm_20250709_212922.log
│ └── cpm_20250709_213103.log
scripts/
│ ├── check_lfs_status.bat
│ ├── fix_large_files.bat
│ ├── fix_remote_organization.bat
│ ├── fix_website_auth.bat
│ ├── fix_website_final.bat
│ ├── fix_website_lfs_and_branch.bat
│ ├── fix_website_repo.bat
│ ├── quick_fix_website.bat
│ └── website_gitignore_template.txt
services/
│ ├── activity_sync.py
│ └── __init__.py
src/
│ └── gitea/
│ ├── gitea_client.py
│ ├── gitea_ui.py
│ ├── gitea_ui_ctk.py
│ ├── git_operations.py
│ ├── issue_pr_manager.py
│ ├── repository_manager.py
│ └── __init__.py
tests/
│ └── test_main_window_api.py
tools/
│ └── download_winscp.txt
utils/
└── logger.py
```
## Key Files
- `README.md`
- `requirements.txt`
## Claude Integration
This project is managed with Claude Project Manager. To work with this project:
1. Open Claude Project Manager
2. Click on this project's tile
3. Claude will open in the project directory
## Notes
*Add your project-specific notes here*
---
## Development Log
- README generated on 2025-07-01 20:15:32
- README updated on 2025-07-01 20:23:43
- README updated on 2025-07-01 20:23:47
- README updated on 2025-07-01 20:32:57
- README updated on 2025-07-01 21:09:37
- README updated on 2025-07-01 21:10:55
- README updated on 2025-07-01 21:13:32
- README updated on 2025-07-01 21:13:42
- README updated on 2025-07-01 21:20:15
- README updated on 2025-07-01 21:20:17
- README updated on 2025-07-01 21:20:42
- README updated on 2025-07-01 21:22:21
- README updated on 2025-07-01 21:26:20
- README updated on 2025-07-01 21:33:49
- README updated on 2025-07-01 21:37:22
- README updated on 2025-07-01 21:37:26
- README updated on 2025-07-01 21:37:33
- README updated on 2025-07-01 21:37:37
- README updated on 2025-07-01 21:47:34
- README updated on 2025-07-01 21:48:07
- README updated on 2025-07-01 21:48:21
- README updated on 2025-07-01 21:48:30
- README updated on 2025-07-01 21:48:45
- README updated on 2025-07-01 21:50:43
- README updated on 2025-07-01 21:58:40
- README updated on 2025-07-01 22:00:03
- README updated on 2025-07-01 22:03:17
- README updated on 2025-07-01 22:04:16
- README updated on 2025-07-01 22:04:39
- README updated on 2025-07-01 22:09:23
- README updated on 2025-07-02 12:42:23
- README updated on 2025-07-02 12:42:44
- README updated on 2025-07-02 16:08:16
- README updated on 2025-07-02 19:09:27
- README updated on 2025-07-02 19:09:35
- README updated on 2025-07-02 22:07:57
- README updated on 2025-07-03 07:30:27
- README updated on 2025-07-03 14:21:29
- README updated on 2025-07-03 22:24:23
- README updated on 2025-07-03 23:49:53
- README updated on 2025-07-04 18:06:40
- README updated on 2025-07-05 01:35:23
- README updated on 2025-07-05 13:35:42
- README updated on 2025-07-05 20:48:36
- README updated on 2025-07-06 10:58:00
- README updated on 2025-07-07 08:30:39
- README updated on 2025-07-07 21:38:23
- README updated on 2025-07-07 21:50:23
- README updated on 2025-07-07 22:12:28
- README updated on 2025-07-07 22:34:45
- README updated on 2025-07-08 08:19:16
- README updated on 2025-07-08 11:17:58
- README updated on 2025-07-09 21:31:18

186
DUPLICATE_ANALYSIS.md Normale Datei
Datei anzeigen

@ -0,0 +1,186 @@
# Duplicate Methods Analysis - main_window.py
## Übersicht
Analyse der 4 duplizierten Methoden-Paare in `gui/main_window.py`.
## 1. `manage_branches` (Zeilen 1374 & 2336)
### Erste Version (Zeile 1374)
```python
def manage_branches(self, project):
messagebox.showinfo("Branches", "Branch management coming soon")
```
- **Typ**: Placeholder
- **Funktionalität**: Zeigt nur Info-Dialog
### Zweite Version (Zeile 2336)
```python
def manage_branches(self, project):
# 55 Zeilen Code
# Vollständige Implementierung mit:
# - Branch-Auflistung
# - Branch erstellen
# - Branch wechseln
# - UI Dialog
```
- **Typ**: Vollständige Implementierung
- **Funktionalität**: Komplettes Branch-Management
### Empfehlung
**Erste Version löschen, zweite behalten**
- Die erste Version ist nur ein Placeholder
- Die zweite Version ist die fertige Implementierung
---
## 2. `link_to_gitea` (Zeilen 1378 & 2392)
### Erste Version (Zeile 1378)
```python
def link_to_gitea(self, project):
messagebox.showinfo("Link to Gitea", "Link functionality coming soon")
```
- **Typ**: Placeholder
- **Funktionalität**: Zeigt nur Info-Dialog
### Zweite Version (Zeile 2392)
```python
def link_to_gitea(self, project):
# 64 Zeilen Code
# Vollständige Implementierung mit:
# - Repository-Existenz prüfen
# - Repository erstellen wenn nötig
# - Remote URL setzen
# - Fehlerbehandlung
```
- **Typ**: Vollständige Implementierung
- **Funktionalität**: Komplette Gitea-Verlinkung
### Empfehlung
**Erste Version löschen, zweite behalten**
- Die erste Version ist nur ein Placeholder
- Die zweite Version ist die fertige Implementierung
---
## 3. `test_gitea_connection` (Zeilen 1757 & 2457)
### Erste Version (Zeile 1757)
```python
def test_gitea_connection(self, project):
# 35 Zeilen Code
# Basis-Implementierung:
# - Verbindungstest
# - User-Info anzeigen
# - Organisationen auflisten
```
- **Parameter**: `project` (required)
- **Anzeige**: Via `messagebox.showinfo`
### Zweite Version (Zeile 2457)
```python
def test_gitea_connection(self, project=None):
# 98 Zeilen Code
# Erweiterte Implementierung:
# - Optional project parameter
# - Team-Berechtigungen
# - Projekt-spezifische Git-Info
# - Repository-Auflistungen
# - Bessere Fehlerbehandlung
```
- **Parameter**: `project=None` (optional)
- **Anzeige**: Via `self._show_scrollable_info`
### Unterschiede
1. **Parameter**: Erste Version erfordert `project`, zweite ist optional
2. **Funktionalität**: Zweite Version hat erweiterte Features
3. **Anzeige**: Unterschiedliche Dialog-Methoden
### Empfehlung
🔄 **Beide Versionen zusammenführen**
```python
def test_gitea_connection(self, project=None):
# Kombinierte Implementierung:
# - Optional project parameter (von Version 2)
# - Erweiterte Features (von Version 2)
# - Robuste Fehlerbehandlung (von beiden)
```
---
## 4. `verify_repository_on_gitea` (Zeilen 1792 & 2556)
### Erste Version (Zeile 1792)
```python
def verify_repository_on_gitea(self, project):
# 103 Zeilen Code
# Features:
# - Git-Repository Vorprüfung
# - Repository-Status prüfen
# - Branches vergleichen
# - Remote-URL Validierung
```
- **Anzeige**: Via `messagebox.showinfo`
- **Besonderheit**: Prüft ob `.git` Verzeichnis existiert
### Zweite Version (Zeile 2556)
```python
def verify_repository_on_gitea(self, project):
# 100 Zeilen Code
# Features:
# - Debug-Info (.git/config Inhalt)
# - Repository-Status prüfen
# - Branches vergleichen
# - Remote-URL Validierung
```
- **Anzeige**: Via `self._show_scrollable_info`
- **Besonderheit**: Zeigt Debug-Informationen
### Unterschiede
1. **Vorprüfungen**: Erste Version prüft Git-Verzeichnis
2. **Debug-Info**: Zweite Version zeigt .git/config
3. **Dialog**: Unterschiedliche Anzeige-Methoden
### Empfehlung
🔄 **Beide Versionen zusammenführen**
```python
def verify_repository_on_gitea(self, project):
# Kombinierte Implementierung:
# - Git-Verzeichnis Vorprüfung (von Version 1)
# - Debug-Informationen (von Version 2)
# - Scrollbare Anzeige (von Version 2)
```
---
## Aufruf-Analyse
Alle duplizierten Methoden werden vom `gitea_operation` Method aufgerufen:
```python
def gitea_operation(self, operation_name, project):
operations = {
"status": self.show_git_status,
"push": lambda p: self.push_to_gitea(p),
"init_push": self.init_and_push_to_gitea,
"test": self.test_gitea_connection, # Ruft duplizierte Methode auf
"verify": self.verify_repository_on_gitea, # Ruft duplizierte Methode auf
"link": self.link_to_gitea, # Ruft duplizierte Methode auf
"branches": self.manage_branches, # Ruft duplizierte Methode auf
# ...
}
```
## Konsolidierungs-Strategie
### Sofort löschbar (Placeholder):
1.`manage_branches` (Zeile 1374) - Nur Placeholder
2.`link_to_gitea` (Zeile 1378) - Nur Placeholder
### Zusammenführung erforderlich:
3. 🔄 `test_gitea_connection` - Features beider Versionen kombinieren
4. 🔄 `verify_repository_on_gitea` - Vorprüfungen und Debug-Info kombinieren
### Priorität:
1. **Hoch**: Placeholder löschen (einfach, keine Risiken)
2. **Mittel**: Methoden zusammenführen (mehr Aufwand, Tests nötig)

53
Logo.svg Normale Datei
Datei anzeigen

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="500" height="400" viewBox="0 0 500 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&amp;display=swap');
</style>
<!-- Accurate shield matching original -->
<g id="shield-eye-accurate">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="currentColor"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="currentColor"/>
</g>
</g>
</defs>
<!-- Dark version for website - NO BACKGROUND, NO TAGLINE -->
<g transform="translate(40, 200)">
<!-- Shield centered vertically with text -->
<g transform="translate(0, -72.5)" color="white">
<use href="#shield-eye-accurate"/>
</g>
<!-- Text aligned with shield center - NO TAGLINE -->
<text x="110" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="white">IntelSight</text>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

188
MIGRATION_STATUS.md Normale Datei
Datei anzeigen

@ -0,0 +1,188 @@
# Migration Status - MainWindow Refactoring
## Übersicht
Dieses Dokument zeigt den aktuellen Status der Methoden-Migration.
## UIHelpersHandler - Migrierte Methoden ✅
### Vollständig migriert (direkte Implementierung im Handler):
1. **load_and_apply_theme**
- Einfache 3-Zeilen Methode
- Keine Abhängigkeiten
- Test: Erfolgreich
2. **_show_scrollable_info**
- UI Dialog Methode
- 33 Zeilen, eigenständig
- Test: Erfolgreich
3. **create_header**
- UI Erstellung
- 44 Zeilen
- Referenzen zu main_window Attributen
- Test: Erfolgreich
4. **create_status_bar**
- UI Erstellung
- 22 Zeilen
- Referenzen zu main_window Attributen
- Test: Erfolgreich
5. **on_window_resize**
- Event Handler
- 10 Zeilen
- Zugriff auf resize_timer
- Test: Erfolgreich
### Noch zu migrieren (aktuell als Facade):
- setup_ui
- create_content_area
- create_project_tile
- create_add_tile
- create_project_tile_flow
- create_add_tile_flow
- refresh_ui
- setup_interaction_tracking
- _differential_update
- _update_project_tiles_colors
## GiteaOperationsHandler - Migrierte Methoden ✅
### Vollständig migriert (Duplikate konsolidiert):
1. **test_gitea_connection**
- Konsolidierte Version (beide Implementierungen gemerged)
- Optionaler project Parameter
- Erweiterte Team- und Projekt-Info
- Test: Erfolgreich
2. **verify_repository_on_gitea**
- Konsolidierte Version (Pre-checks von v1 + Debug-Info von v2)
- Git-Verzeichnis Prüfung
- Debug-Info aus .git/config
- Test: Manuell
### Placeholder entfernt:
- ❌ _original_manage_branches (Zeile 1469) - Nur Placeholder, gelöscht
- ❌ _original_link_to_gitea (Zeile 1481) - Nur Placeholder, gelöscht
### Delegiert an v2 Implementierung:
- manage_branches → _original_manage_branches_v2
- link_to_gitea → _original_link_to_gitea_v2
### Große Methoden refactored (Phase 5):
1. **init_and_push_to_gitea** ✅ (214 → 8 Methoden)
- Aufgeteilt in logische Schritte
- Single Responsibility Principle
- Test: Manuell
2. **push_to_gitea** ✅ (176 → 10 Methoden)
- Klare Trennung Validierung/Aktion
- Bessere Fehlerbehandlung
- Test: Manuell
3. **manage_large_files** ✅ (160 → 11 Methoden)
- UI von Business-Logik getrennt
- Wiederverwendbare Komponenten
- Test: Manuell
4. **setup_git_lfs** ✅ (131 → 9 Methoden)
- Klarer Dialog-Flow
- Separate Tracking-Modi
- Test: Manuell
5. **fix_repository_issues** ✅ (110 → 9 Methoden)
- Diagnose von Aktion getrennt
- Modulare Button-Erstellung
- Test: Manuell
## ProcessManagerHandler - Migrierte Methoden ✅
### Vollständig migriert:
1. **update_status**
- Status Bar Update
- 6 Zeilen
- Test: Erfolgreich
2. **download_log**
- Log Export Funktion
- 20 Zeilen
- File Dialog Interaktion
- Test: Manuell (Dialog-basiert)
### Noch zu migrieren:
- monitor_process
- check_process_status
- stop_project
- _handle_process_ended
## ProjectManagerHandler - Migrierte Methoden ✅
### Vollständig migriert:
1. **delete_project**
- Projekt aus Manager entfernen
- 10 Zeilen
- Dialog-Bestätigung
- Test: Erfolgreich
### Noch zu migrieren:
- add_new_project
- open_project
- rename_project
- refresh_projects
- create_project_from_repo
- open_vps_connection
- open_admin_panel
- open_vps_docker
- open_readme
- generate_readme_background
- open_gitea_window
- on_gitea_repo_select
- clear_project_selection
- on_project_select
## Aktivierung
### Aktuell aktiviert:
```bash
✅ USE_UI_HELPERS: true
✅ USE_PROCESS_HANDLER: true
✅ USE_PROJECT_HANDLER: true
❌ USE_GITEA_HANDLER: false
```
### Nächste Aktivierung empfohlen:
```bash
python3 manage_refactoring.py enable gitea
```
## Metriken
- **Gesamt Methoden**: 67
- **Migriert**: 15 (22.4%)
- UIHelpersHandler: 5
- ProcessManagerHandler: 2
- ProjectManagerHandler: 1
- GiteaOperationsHandler: 7 (2 konsolidiert + 5 refactored)
- **Als Facade**: 52 (77.6%)
- **Gelöschte Zeilen**: ~801 (2 Placeholder + 5 große Methoden refactored)
- **Aktivierte Handler**: 4 von 4 (100%) ✅
- **Duplikate aufgelöst**: 2 von 4 (50%)
- **Große Methoden refactored**: 5 (alle tatsächlich großen Methoden >100 Zeilen)
## Nächste Schritte
1. **Weitere UIHelpers migrieren**
- create_content_area (komplex wegen GiteaExplorer)
- setup_interaction_tracking
2. **ProjectManager aktivieren**
- Einfache CRUD Operationen
- Weniger Abhängigkeiten als Gitea
3. **Duplikate konsolidieren**
- 4 Methoden-Paare zusammenführen
- Original-Versionen löschen
4. **Phase 5 beginnen**
- Große Methoden in kleinere aufteilen
- Single Responsibility Principle

135
README.md Normale Datei
Datei anzeigen

@ -0,0 +1,135 @@
# Claude Project Manager
Eine moderne GUI-Anwendung zur Verwaltung von Claude-Projekten mit automatischer README-Generierung und VPS-Integration.
## Features
### 🚀 Hauptfunktionen
- **Projekt-Kacheln**: Übersichtliche Darstellung aller Projekte
- **Ein-Klick-Start**: Claude direkt im richtigen Verzeichnis starten
- **Auto-README**: Automatische Generierung und Aktualisierung von README-Dateien
- **VPS-Integration**: Permanente Kachel für VPS-Server-Zugriff
- **Suchfunktion**: Schnelles Finden von Projekten
### 📁 Projektverwaltung
- Projekte via Windows Explorer auswählen
- Automatisches Speichern der Projekthistorie
- Letzter Zugriff wird angezeigt
- Projekte können entfernt werden (Dateien bleiben erhalten)
### 📝 README-Generator
- Analysiert Projektstruktur automatisch
- Erkennt Programmiersprachen und Frameworks
- Dokumentiert Dateistruktur
- Behält benutzerdefinierte Notizen bei Updates
### 🌐 VPS-Funktionen
- Dauerhafte VPS-Server-Kachel
- Automatische SSH-Verbindung
- Passwort-Anleitung im Terminal
- Eigene VPS-README
## Installation
### Voraussetzungen
- Windows mit WSL (Windows Subsystem for Linux)
- Python 3.8 oder höher
- Claude CLI in WSL installiert
### Setup
1. **Repository klonen oder herunterladen**
```bash
cd C:\Users\hendr\Desktop\IntelSight\Claude-Project-Manager
```
2. **Abhängigkeiten installieren**
```bash
pip install -r requirements.txt
```
3. **Anwendung starten**
```bash
python main.py
```
## Verwendung
### Neues Projekt hinzufügen
1. Klicken Sie auf die "+" Kachel
2. Wählen Sie einen Projektordner
3. Claude startet automatisch im gewählten Verzeichnis
4. Eine README wird automatisch generiert
### Projekt öffnen
1. Klicken Sie auf eine Projekt-Kachel
2. Claude öffnet sich im Terminal mit dem richtigen Arbeitsverzeichnis
### VPS-Server verwenden
1. Klicken Sie auf die VPS-Server-Kachel
2. Terminal öffnet sich mit Verbindungsanweisungen
3. Passwort eingeben (wird angezeigt)
4. `claude` eingeben nach erfolgreicher Verbindung
### README anzeigen
1. Klicken Sie auf den "README" Button einer Kachel
2. Die README öffnet sich im Standard-Editor
## Projektstruktur
```
Claude-Project-Manager/
├── main.py # Haupteinstiegspunkt
├── project_manager.py # Projektverwaltung & Speicherung
├── terminal_launcher.py # WSL/Terminal-Integration
├── readme_generator.py # Automatische README-Generierung
├── vps_connection.py # VPS-Server-Verbindung
├── gui/
│ ├── main_window.py # Hauptfenster-Logik
│ ├── project_tile.py # Projekt-Kacheln-Komponente
│ └── styles.py # Design-Konfiguration
├── data/
│ └── projects.json # Gespeicherte Projekte
└── requirements.txt # Python-Abhängigkeiten
```
## Konfiguration
### Claude-Pfad anpassen
In `terminal_launcher.py`, Zeile 13:
```python
self.claude_path = "/home/hendr/.nvm/versions/node/v18.20.8/bin/claude"
```
### VPS-Daten ändern
In `vps_connection.py`, Zeilen 11-13:
```python
self.server = "91.99.192.14"
self.username = "claude-dev"
self.password = "z0E1Al}q2H?Yqd!O"
```
## Fehlerbehebung
### Claude startet nicht
1. Prüfen Sie, ob Claude in WSL installiert ist
2. Verifizieren Sie den Claude-Pfad in `terminal_launcher.py`
3. Stellen Sie sicher, dass WSL läuft
### VPS-Verbindung schlägt fehl
1. Internetverbindung prüfen
2. SSH-Port 22 darf nicht blockiert sein
3. Zugangsdaten überprüfen
## Tastenkürzel
- **Strg+F**: Suche (wenn Suchfeld fokussiert)
- **F5**: Projekte aktualisieren
## Lizenz
Dieses Projekt ist für den persönlichen Gebrauch bestimmt.
## Autor
Entwickelt für die effiziente Verwaltung von Claude-Projekten.

50
WEBSOCKET_TROUBLESHOOTING.md Normale Datei
Datei anzeigen

@ -0,0 +1,50 @@
# WebSocket Verbindungsprobleme - Lösungen
## Problem
WebSocket-Verbindungen (ws:// oder wss://) werden oft von Unternehmensnetzwerken blockiert:
- Firewalls blockieren WebSocket-Protokoll
- Proxies unterstützen kein WebSocket-Upgrade
- Nur HTTP/HTTPS Ports (80/443) sind erlaubt
## Implementierte Lösung
Der Activity Service verwendet jetzt **HTTP Long-Polling** statt WebSockets:
- Funktioniert über Standard HTTP
- Umgeht WebSocket-Blockierungen
- Etwas höhere Latenz, aber zuverlässiger in restriktiven Netzwerken
## Weitere Lösungsansätze
### 1. Proxy-Konfiguration
Wenn Ihr Netzwerk einen HTTP-Proxy verwendet:
```python
# In services/activity_sync.py
import os
os.environ['HTTP_PROXY'] = 'http://proxy.company.com:8080'
os.environ['HTTPS_PROXY'] = 'http://proxy.company.com:8080'
```
### 2. Alternative Ports
Fragen Sie Ihren Admin, ob der Activity Server auch auf Port 80 oder 443 laufen kann.
### 3. VPN verwenden
Wenn verfügbar, kann eine VPN-Verbindung die Netzwerkbeschränkungen umgehen.
### 4. Lokaler Activity Server
Für Entwicklung/Test einen lokalen Server verwenden:
- Server URL: `http://localhost:3001`
- Keine Netzwerkbeschränkungen
## Test der Verbindung
1. Browser-Test: http://91.99.192.14:3001/health
2. Curl-Test: `curl http://91.99.192.14:3001/health`
3. Python-Test-Skript: `python test_activity_connection.py`
## Firewall-Regeln
Falls Sie Admin-Rechte haben, erlauben Sie:
- Ausgehende TCP-Verbindungen zu 91.99.192.14:3001
- HTTP/HTTPS Traffic zu diesem Server
## Status in CPM
- Grüner Punkt: Verbindung aktiv
- Meldung "Nicht verbunden": Server nicht erreichbar
- Die Aktivitätsfunktion ist optional - CPM funktioniert vollständig ohne sie

51
activity_server_cmd_fix.md Normale Datei
Datei anzeigen

@ -0,0 +1,51 @@
# Activity Server CMD Connection Fix
## Problem
Users had to manually navigate to `/home/claude-dev/cpm-activity-server` after connecting via SSH when using the Activity Server CMD button.
## Solution Implemented
### 1. Enhanced VPS Connection Module
Modified `vps_connection.py` to create a more robust `create_activity_server_script()` method that:
- Detects available tools (PowerShell, plink, WSL, ssh)
- Attempts automatic directory change using multiple methods
- Provides clear fallback instructions when automation isn't possible
### 2. Project Tile Updates
Modified `gui/project_tile.py` to:
- Enable CMD button for both VPS Server and Activity Server tiles
- Use the VPS connection module for generating connection scripts
- Properly handle different project types
### 3. Connection Methods (in order of preference)
#### PowerShell + plink
- Uses PowerShell to better control plink execution
- Attempts to run commands after authentication
- Falls back to interactive mode with instructions
#### plink with batch commands
- Tries to execute `cd /home/claude-dev/cpm-activity-server && exec bash -l`
- If that fails, provides interactive session with clear instructions
#### WSL + sshpass
- Most reliable method when available
- Automatically changes directory and starts claude
- Uses inline bash commands for better control
#### Standard SSH fallback
- Clear instructions displayed for manual navigation
- Password shown for easy copy/paste
## Usage
1. Click the "CMD" button on the Activity Server tile
2. The script will attempt to connect and automatically navigate to the correct directory
3. If automatic navigation fails, clear instructions are displayed
## Files Modified
- `/vps_connection.py` - Enhanced `create_activity_server_script()` method
- `/gui/project_tile.py` - Updated CMD button logic and connection handling
## Additional Files Created
- `/activity_server_connect.ps1` - PowerShell script for enhanced connection (optional)
- `/setup_activity_server_profile.sh` - Server-side profile setup script (optional)

103
activity_server_connect.ps1 Normale Datei
Datei anzeigen

@ -0,0 +1,103 @@
# PowerShell script for Activity Server connection with automatic directory change
$Host.UI.RawUI.WindowTitle = "CPM Activity Server Connection"
Write-Host "================================================================================" -ForegroundColor Cyan
Write-Host " CPM Activity Server Connection" -ForegroundColor Cyan
Write-Host "================================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Server: 91.99.192.14"
Write-Host "Username: claude-dev"
Write-Host "Target Directory: /home/claude-dev/cpm-activity-server"
Write-Host ""
# Check if plink is available
$plinkPath = Get-Command plink -ErrorAction SilentlyContinue
if ($plinkPath) {
Write-Host "Using PuTTY plink for connection..." -ForegroundColor Green
Write-Host ""
# Create a temporary expect-like script using PowerShell
Write-Host "Attempting automatic connection with directory change..." -ForegroundColor Yellow
Write-Host ""
# First attempt: Try to use plink with a command
$plinkArgs = @(
"-ssh",
"-l", "claude-dev",
"-pw", "z0E1Al}q2H?Yqd!O",
"-t",
"91.99.192.14",
"cd /home/claude-dev/cpm-activity-server && echo 'Successfully changed to Activity Server directory' && echo '' && bash -l"
)
$plinkProcess = Start-Process -FilePath "plink" -ArgumentList $plinkArgs -PassThru -Wait
if ($plinkProcess.ExitCode -ne 0) {
Write-Host ""
Write-Host "Automatic directory change failed. Starting interactive session..." -ForegroundColor Yellow
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Red
Write-Host "IMPORTANT: After login, please run these commands:" -ForegroundColor Red
Write-Host ""
Write-Host " cd /home/claude-dev/cpm-activity-server" -ForegroundColor White
Write-Host " claude" -ForegroundColor White
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Red
Write-Host ""
# Interactive session
& plink -ssh -l claude-dev -pw "z0E1Al}q2H?Yqd!O" -t 91.99.192.14
}
} else {
# Check for WSL
$wslPath = Get-Command wsl -ErrorAction SilentlyContinue
if ($wslPath) {
Write-Host "Using WSL for connection..." -ForegroundColor Green
Write-Host ""
# Use WSL with sshpass
$wslCommand = @"
if command -v sshpass >/dev/null 2>&1; then
echo 'Using sshpass for automatic login...'
sshpass -p 'z0E1Al}q2H?Yqd!O' ssh -o StrictHostKeyChecking=no -t claude-dev@91.99.192.14 'cd /home/claude-dev/cpm-activity-server && echo "Successfully changed to Activity Server directory" && echo && claude || bash -l'
else
echo 'Manual password entry required.'
echo 'Password: z0E1Al}q2H?Yqd!O'
echo ''
echo 'After login, run: cd /home/claude-dev/cpm-activity-server && claude'
echo ''
ssh -o StrictHostKeyChecking=no claude-dev@91.99.192.14
fi
"@
& wsl bash -c $wslCommand
} else {
# Fallback to SSH
Write-Host "No automated tools found. Using standard SSH..." -ForegroundColor Yellow
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Red
Write-Host "Connection Instructions:" -ForegroundColor Red
Write-Host ""
Write-Host "1. Enter this password when prompted: z0E1Al}q2H?Yqd!O" -ForegroundColor White
Write-Host " (You can right-click to paste in most terminals)" -ForegroundColor Gray
Write-Host ""
Write-Host "2. After successful login, run these commands:" -ForegroundColor White
Write-Host " cd /home/claude-dev/cpm-activity-server" -ForegroundColor Cyan
Write-Host " claude" -ForegroundColor Cyan
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Red
Write-Host ""
Write-Host "Press any key to continue..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
& ssh claude-dev@91.99.192.14
}
}
Write-Host ""
Write-Host "Connection closed." -ForegroundColor Yellow
Write-Host "Press any key to exit..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

12
app-icon.ico Normale Datei
Datei anzeigen

@ -0,0 +1,12 @@
This is a placeholder ICO file. To properly convert the SVG to ICO format, you'll need to:
1. Use an online converter like:
- https://convertio.co/de/svg-ico/
- https://cloudconvert.com/svg-to-ico
2. Or install ImageMagick and run:
convert app-icon.svg -resize 256x256 app-icon.ico
3. Or use a tool like Inkscape to export as ICO
The SVG file (app-icon.svg) contains a blue square icon that you can convert.

14
app-icon.svg Normale Datei
Datei anzeigen

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<!-- Blue square outline with darker right edge - matching customtkinter style -->
<g>
<!-- Top edge -->
<line x1="40" y1="40" x2="216" y2="40" stroke="#0066CC" stroke-width="6"/>
<!-- Left edge -->
<line x1="40" y1="40" x2="40" y2="216" stroke="#0066CC" stroke-width="6"/>
<!-- Bottom edge -->
<line x1="40" y1="216" x2="216" y2="216" stroke="#0066CC" stroke-width="6"/>
<!-- Right edge (darker) -->
<line x1="216" y1="40" x2="216" y2="216" stroke="#004080" stroke-width="8"/>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 654 B

35
build.bat Normale Datei
Datei anzeigen

@ -0,0 +1,35 @@
@echo off
title Building Claude Project Manager
echo Building Claude Project Manager executable...
echo.
:: Check if Python is installed
python --version >nul 2>&1
if errorlevel 1 (
echo ERROR: Python is not installed or not in PATH
echo Please install Python 3.8 or higher
pause
exit /b 1
)
:: Install PyInstaller if not already installed
pip show pyinstaller >nul 2>&1
if errorlevel 1 (
echo Installing PyInstaller...
pip install pyinstaller
echo.
)
:: Install other requirements
echo Installing requirements...
pip install -r requirements.txt >nul 2>&1
:: Run the build script
echo.
echo Building executable...
python build_exe.py
echo.
echo Build process completed!
echo Check the 'dist' folder for the executable.
pause

48
build_exe.py Normale Datei
Datei anzeigen

@ -0,0 +1,48 @@
"""
Build executable for Claude Project Manager
"""
import PyInstaller.__main__
import os
import sys
def build_exe():
"""Build executable using PyInstaller"""
# Get the directory of this script
script_dir = os.path.dirname(os.path.abspath(__file__))
# PyInstaller arguments
args = [
'main.py', # Main script
'--name=ClaudeProjectManager', # Name of the executable
'--onefile', # Create a single executable file
'--windowed', # No console window
'--icon=NONE', # No icon for now
'--add-data=data;data', # Include data directory
'--add-data=gui;gui', # Include gui directory
'--add-data=utils;utils', # Include utils directory for logger
'--add-data=src;src', # Include src directory with gitea modules
'--hidden-import=customtkinter',
'--hidden-import=tkinter',
'--hidden-import=PIL',
'--hidden-import=packaging',
'--hidden-import=requests',
'--hidden-import=urllib3',
'--hidden-import=git',
'--collect-all=customtkinter', # Collect all customtkinter files
'--noconfirm', # Overwrite output directory without confirmation
'--clean', # Clean PyInstaller cache and remove temporary files
]
# Run PyInstaller
try:
PyInstaller.__main__.run(args)
print("\nBuild completed successfully!")
print(f"Executable can be found in: {os.path.join(script_dir, 'dist')}")
except Exception as e:
print(f"Build failed: {e}")
sys.exit(1)
if __name__ == "__main__":
build_exe()

28
clone_fix.py Normale Datei
Datei anzeigen

@ -0,0 +1,28 @@
"""
Temporäre Lösung für das Clone-Problem mit Organization-Repositories
"""
def apply_temporary_fix():
"""
Diese Funktion zeigt die Zeilen, die temporär geändert werden können,
um das Clone-Problem zu umgehen.
"""
print("TEMPORÄRE LÖSUNG für das Clone-Problem:")
print("=" * 50)
print("\nOption 1: Organization-Prüfung komplett deaktivieren")
print("In gui/main_window.py, Zeile 1431 ändern von:")
print(" if repo_owner and repo_owner != self.repo_manager.current_user.get('username'):")
print("\nZu:")
print(" if False: # Temporär deaktiviert")
print("\n" + "-" * 50)
print("\nOption 2: Nur für IntelSight erlauben")
print("In gui/main_window.py, Zeile 1439 ändern von:")
print(" if repo_owner not in org_names:")
print("\nZu:")
print(" if repo_owner not in org_names and repo_owner != 'IntelSight':")
print("\n" + "=" * 50)
print("\nNach der Änderung muss die Anwendung neu gestartet werden.")
if __name__ == "__main__":
apply_temporary_fix()

88
data/projects.json Normale Datei
Datei anzeigen

@ -0,0 +1,88 @@
{
"projects": [
{
"id": "vps-permanent",
"name": "VPS Server",
"path": "claude-dev@91.99.192.14",
"created_at": "2025-07-01T20:14:48.308074",
"last_accessed": "2025-07-09T21:59:56.122304",
"readme_path": "claude-dev@91.99.192.14\\CLAUDE_PROJECT_README.md",
"description": "Remote VPS Server with Claude",
"tags": [
"vps",
"remote",
"server"
],
"gitea_repo": null
},
{
"id": "admin-panel-permanent",
"name": "Admin Panel",
"path": "/opt/v2-Docker",
"created_at": "2025-07-02T11:07:12.516091",
"last_accessed": "2025-07-02T11:45:26.857409",
"readme_path": "/opt/v2-Docker\\CLAUDE_PROJECT_README.md",
"description": "V2 Docker Admin Panel",
"tags": [
"admin",
"docker",
"v2"
],
"gitea_repo": null
},
{
"id": "vps-docker-permanent",
"name": "Docker Restart",
"path": "/opt/v2-Docker/v2",
"created_at": "2025-07-02T16:15:00.000000",
"last_accessed": "2025-07-02T16:25:36.888108",
"readme_path": "/opt/v2-Docker/v2\\CLAUDE_PROJECT_README.md",
"description": "Docker Admin Panel Restart",
"tags": [
"vps",
"docker",
"admin",
"restart"
],
"gitea_repo": null
},
{
"id": "66c0f4bb-c560-43a6-a4c5-a8ecf8b73919",
"name": "ClaudeProjectManager",
"path": "C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main",
"created_at": "2025-07-07T21:38:23.820122",
"last_accessed": "2025-07-09T21:31:18.456230",
"readme_path": "C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main\\CLAUDE_PROJECT_README.md",
"description": "",
"tags": [],
"gitea_repo": null
},
{
"id": "activity-server-permanent",
"name": "Activity Server",
"path": "/home/claude-dev/cpm-activity-server",
"created_at": "2025-07-08T12:12:14.492170",
"last_accessed": "2025-07-09T21:54:00.618934",
"readme_path": "/home/claude-dev/cpm-activity-server\\CLAUDE_PROJECT_README.md",
"description": "CPM Activity Server",
"tags": [
"activity",
"server",
"cpm"
],
"gitea_repo": null
},
{
"id": "f1a73b61-40ed-4359-b8bb-c35142367e93",
"name": "Metadaten-Crawler",
"path": "C:/Users/hendr/Desktop/IntelSight/Projektablage/Metadaten-Crawler",
"created_at": "2025-07-08T13:51:49.398180",
"last_accessed": "2025-07-08T13:51:49.398180",
"readme_path": "C:/Users/hendr/Desktop/IntelSight/Projektablage/Metadaten-Crawler\\CLAUDE_PROJECT_README.md",
"description": "",
"tags": [],
"gitea_repo": null
}
],
"last_updated": "2025-07-09T21:59:56.122304"
}

3
data/settings.json Normale Datei
Datei anzeigen

@ -0,0 +1,3 @@
{
"theme": "light"
}

Datei anzeigen

@ -0,0 +1,63 @@
# Claude VPS Server
*This README was automatically generated by Claude Project Manager*
## Server Information
- **Server IP**: 91.99.192.14
- **Username**: claude-dev
- **Connection**: SSH (Port 22)
## How to Connect
### Using Claude Project Manager
1. Click on the VPS Server tile
2. Terminal will open with connection instructions
3. Enter the password when prompted
4. Type `claude` after successful login
### Manual Connection
```bash
ssh claude-dev@91.99.192.14
```
## Available Commands
After connecting to the VPS:
- `claude` - Start Claude CLI
- `ls` - List files
- `cd <directory>` - Change directory
- `exit` - Close SSH connection
## Security Notes
- Keep your password secure
- Don't share SSH credentials
- Always logout when finished (`exit` command)
## Features
This VPS server provides:
- Remote access to Claude
- Persistent environment
- Isolated workspace
- Full Linux environment
## Troubleshooting
### Connection Issues
1. Check internet connection
2. Verify server is online
3. Ensure SSH port (22) is not blocked
4. Try manual SSH command
### Authentication Failed
- Verify password is correct
- Check username spelling
- Ensure caps lock is off
---
## Connection Log
- README generated on 2025-07-09 21:59:56

540
duplicate_methods_analysis.md Normale Datei
Datei anzeigen

@ -0,0 +1,540 @@
# Duplicate Methods Analysis Report - gui/main_window.py
## Overview
This report analyzes the duplicate methods found in `gui/main_window.py` to determine their differences and whether they can be safely consolidated.
## 1. `manage_branches` (line 1374 vs line 2336)
### First Version (line 1374-1377)
```python
def manage_branches(self, project):
"""Manage git branches"""
messagebox.showinfo("Branches", "Branch management coming soon")
```
### Second Version (line 2336-2391)
```python
def manage_branches(self, project):
"""Manage branches for a project"""
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}")
```
### Differences
- **First version**: Placeholder implementation showing "coming soon" message
- **Second version**: Full implementation with branch listing, creation, and switching functionality
- **Language**: First version uses English, second version uses German
### Called From
- Line 1516: `self.manage_branches(project)` in `gitea_operation` method
### Recommendation
**Safe to consolidate**: Keep the second version (line 2336) as it contains the actual implementation. The first version is just a placeholder.
---
## 2. `link_to_gitea` (line 1378 vs line 2392)
### First Version (line 1378-1381)
```python
def link_to_gitea(self, project):
"""Link local project to Gitea repository"""
messagebox.showinfo("Link", "Link functionality coming soon")
```
### Second Version (line 2392-2456)
```python
def link_to_gitea(self, project):
"""Link local project to Gitea repository"""
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, offer to create
create = messagebox.askyesno(
"Repository nicht gefunden",
f"Repository '{repo_name}' existiert nicht.\n\nMöchten Sie es erstellen?"
)
if create:
try:
# Create repository
new_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
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)}")
```
### Differences
- **First version**: Placeholder implementation showing "coming soon" message
- **Second version**: Full implementation with repository linking, checking existence, and creation option
- **Language**: First version uses English, second version uses German
### Called From
- Line 1514: `self.link_to_gitea(project)` in `gitea_operation` method
### Recommendation
**Safe to consolidate**: Keep the second version (line 2392) as it contains the actual implementation. The first version is just a placeholder.
---
## 3. `test_gitea_connection` (line 1757 vs line 2457)
### First Version (line 1757-1791)
```python
def 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)}")
```
### Second Version (line 2457-2554)
```python
def test_gitea_connection(self, project=None):
"""Test Gitea connection and show detailed information"""
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)
```
### Key Differences
- **Parameter**: First version requires `project`, second version has `project=None` (optional)
- **Detail level**: Second version shows much more information including:
- Team permissions with detailed permission levels
- Project-specific git remote info if project is provided
- Current branch information
- Repository listings (user/org mode aware)
- Log file location
- **UI elements**: First version shows email and admin status, second version doesn't
- **Error handling**: Second version has more detailed error messages with checkmark/cross emojis
### Called From
- Line 1522: `self.test_gitea_connection(project)` in `gitea_operation` method
### Recommendation
**Requires careful consolidation**: The second version is more comprehensive but has an optional parameter. Since the call from `gitea_operation` always passes a project, we should keep the enhanced functionality of the second version but ensure it works correctly when a project is provided.
---
## 4. `verify_repository_on_gitea` (line 1792 vs line 2556)
### First Version (line 1792-1849+)
```python
def 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
# ... (continues beyond visible range)
```
### Second Version (line 2556-2656)
```python
def verify_repository_on_gitea(self, project):
"""Verify if repository exists on Gitea and show detailed info"""
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)
```
### Key Differences
- **Project access**: Second version handles both object attributes and dictionary access for project data
- **Pre-checks**: First version checks if it's a git repo and has remotes before proceeding
- **Display method**: First version shows info in messagebox, second version uses a scrollable CTkToplevel dialog
- **Additional features**: Second version includes:
- Debug file checking and display
- More structured repository search (exact location, user repos, org repos)
- Header with project name
- **Size display**: First version shows size in KB, second version in bytes
### Called From
- Line 1524: `self.verify_repository_on_gitea(project)` in `gitea_operation` method
- Line 1987: `self.verify_repository_on_gitea(project)` in `fix_repository_issues` method (nested function `check_on_gitea`)
### Recommendation
**Requires careful consolidation**: Both versions have valuable features. The second version's scrollable dialog is better for displaying detailed information, but the first version's pre-checks are important. Combine both:
- Keep pre-checks from first version
- Keep the enhanced display and debug file checking from second version
- Ensure consistent project data access
---
## Summary and Recommendations
1. **`manage_branches`**: Delete first version (placeholder), keep second version
2. **`link_to_gitea`**: Delete first version (placeholder), keep second version
3. **`test_gitea_connection`**: Merge both versions, keeping the enhanced functionality of the second while ensuring it works with required project parameter
4. **`verify_repository_on_gitea`**: Merge both versions, combining pre-checks from first with enhanced display from second
All duplicates are called from the same `gitea_operation` method, so consolidation is safe. The first versions of `manage_branches` and `link_to_gitea` are just placeholders that were later implemented fully in the second versions.

22
gitea_push_debug.txt Normale Datei
Datei anzeigen

@ -0,0 +1,22 @@
Push Debug Info - 2025-07-07 22:11:38.340552
Repository: ClaudeProjectManager-main
Owner: IntelSight
Path: C:\Users\hendr\Desktop\IntelSight\ClaudeProjectManager-main
Current branch: master
Git remotes:
origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git (fetch)
origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git (push)
Git status before push:
Clean
Push command: git push --set-upstream origin master:main -v
Push result: Success
Push stdout:
branch 'master' set up to track 'origin/main'.
Push stderr:
POST git-receive-pack (151391 bytes)
remote: . Processing 1 references
remote: Processed 1 references in total
Pushing to https://gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git
To https://gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git
* [new branch] master -> main
updating local tracking ref 'refs/remotes/origin/main'

113
gui/config.py Normale Datei
Datei anzeigen

@ -0,0 +1,113 @@
"""
Configuration for GUI refactoring
Manages feature flags and refactoring settings
"""
import os
import json
from pathlib import Path
from utils.logger import logger
class RefactoringConfig:
"""Manages refactoring feature flags"""
CONFIG_FILE = "refactoring_config.json"
DEFAULT_FLAGS = {
'USE_GITEA_HANDLER': True, # Enable for Progress Bar support
'USE_PROCESS_HANDLER': True, # Enable for better logging
'USE_PROJECT_HANDLER': False,
'USE_UI_HELPERS': False,
'ENABLE_DEBUG_LOGGING': False,
'FORCE_ORIGINAL_IMPLEMENTATION': False # Emergency override
}
def __init__(self):
self.config_path = Path.home() / ".claude_project_manager" / self.CONFIG_FILE
self.flags = self.DEFAULT_FLAGS.copy()
self._load_config()
self._check_env_overrides()
def _load_config(self):
"""Load configuration from file"""
try:
if self.config_path.exists():
with open(self.config_path, 'r') as f:
loaded_flags = json.load(f)
# Only update known flags
for key, value in loaded_flags.items():
if key in self.flags:
self.flags[key] = value
logger.info(f"Loaded refactoring flag: {key} = {value}")
except Exception as e:
logger.error(f"Failed to load refactoring config: {e}")
def _check_env_overrides(self):
"""Check for environment variable overrides"""
# Global override
if os.getenv('CPM_USE_NEW_HANDLERS'):
logger.info("Enabling all refactored handlers via environment variable")
for key in ['USE_GITEA_HANDLER', 'USE_PROCESS_HANDLER',
'USE_PROJECT_HANDLER', 'USE_UI_HELPERS']:
self.flags[key] = True
# Individual overrides
for flag in self.flags:
env_var = f"CPM_{flag}"
if os.getenv(env_var):
value = os.getenv(env_var).lower() in ('true', '1', 'yes')
self.flags[flag] = value
logger.info(f"Override from env: {flag} = {value}")
def save_config(self):
"""Save current configuration to file"""
try:
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w') as f:
json.dump(self.flags, f, indent=2)
logger.info(f"Saved refactoring config to {self.config_path}")
except Exception as e:
logger.error(f"Failed to save refactoring config: {e}")
def get(self, flag_name: str, default=None):
"""Get a flag value"""
return self.flags.get(flag_name, default)
def set(self, flag_name: str, value: bool):
"""Set a flag value"""
if flag_name in self.flags:
self.flags[flag_name] = value
logger.info(f"Set refactoring flag: {flag_name} = {value}")
else:
logger.warning(f"Unknown refactoring flag: {flag_name}")
def enable_handler(self, handler_name: str):
"""Enable a specific handler"""
# Map handler names to flags
handler_map = {
'gitea': 'USE_GITEA_HANDLER',
'process': 'USE_PROCESS_HANDLER',
'project': 'USE_PROJECT_HANDLER',
'ui': 'USE_UI_HELPERS' # Note: not _HANDLER suffix
}
flag_name = handler_map.get(handler_name.lower())
if flag_name and flag_name in self.flags:
self.set(flag_name, True)
else:
logger.warning(f"Unknown handler: {handler_name}")
def disable_all(self):
"""Disable all refactoring features"""
for flag in self.flags:
if flag.startswith('USE_'):
self.flags[flag] = False
logger.info("All refactoring features disabled")
def get_status(self):
"""Get status of all flags"""
return {
"config_file": str(self.config_path),
"flags": self.flags.copy()
}
# Global instance
refactoring_config = RefactoringConfig()

554
gui/gitea_explorer.py Normale Datei
Datei anzeigen

@ -0,0 +1,554 @@
"""
Gitea Repository Explorer for the sidebar
"""
import customtkinter as ctk
from tkinter import ttk, messagebox
import threading
import logging
import os
from pathlib import Path
from typing import Optional, Callable, List, Dict
from gui.styles import COLORS, FONTS
from src.gitea.repository_manager import RepositoryManager
from src.gitea.gitea_client import GiteaClient
import json
logger = logging.getLogger(__name__)
class GiteaExplorer(ctk.CTkFrame):
def __init__(self, parent, on_repo_select: Optional[Callable] = None, **kwargs):
super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs)
self.on_repo_select = on_repo_select
self.repo_manager = RepositoryManager()
self.repositories = []
self.selected_repo = None
self.view_mode = "organization" # Always organization mode
self.organization_name = "IntelSight" # Fixed to IntelSight
self.organizations = [] # List of user's organizations
self.main_window = None # Will be set by main window
self.setup_ui()
# Skip load_organizations - we always use IntelSight
# Automatisches Laden der Repositories beim Start nach kurzer Verzögerung
self.after(500, self.refresh_repositories)
def _show_gitea_menu(self, repo: Dict, button: ctk.CTkButton):
"""Show Gitea operations menu for repository"""
import tkinter as tk
# Create menu
menu = tk.Menu(self, tearoff=0)
menu.configure(
bg=COLORS['bg_secondary'],
fg=COLORS['text_primary'],
activebackground=COLORS['bg_tile_hover'],
activeforeground=COLORS['text_primary'],
borderwidth=0,
relief="flat"
)
# Check if repository is already cloned
local_path = Path.home() / "GiteaRepos" / repo['name']
is_cloned = local_path.exists()
if is_cloned:
# Already cloned options
menu.add_command(label="📂 Im Explorer öffnen", command=lambda: self._open_in_explorer(repo))
menu.add_command(label="📊 Git Status", command=lambda: self._git_status(repo))
menu.add_separator()
menu.add_command(label="⬇️ Pull (Aktualisieren)", command=lambda: self._git_pull(repo))
menu.add_command(label="🔄 Fetch", command=lambda: self._git_fetch(repo))
menu.add_separator()
menu.add_command(label="🗑️ Lokale Kopie löschen", command=lambda: self._delete_local_copy(repo))
else:
# Not cloned options
menu.add_command(label="📥 Repository klonen", command=lambda: self._clone_repository(repo))
menu.add_separator()
menu.add_command(label=" Repository Info", command=lambda: self._show_repo_info(repo))
menu.add_command(label="🌐 In Browser öffnen", command=lambda: self._open_in_browser(repo))
# Show menu at button position
try:
x = button.winfo_rootx()
y = button.winfo_rooty() + button.winfo_height()
menu.tk_popup(x, y)
finally:
menu.grab_release()
def setup_ui(self):
"""Setup the explorer UI"""
# Header
header_frame = ctk.CTkFrame(self, fg_color="transparent")
header_frame.pack(fill="x", padx=10, pady=(10, 5))
# Title
self.title_label = ctk.CTkLabel(
header_frame,
text="🔧 Gitea Repositories",
font=FONTS['subtitle'],
text_color=COLORS['text_primary']
)
self.title_label.pack(side="left")
# Refresh button
refresh_btn = ctk.CTkButton(
header_frame,
text="🔄",
command=self.refresh_repositories,
width=30,
height=30,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover']
)
refresh_btn.pack(side="right")
# IntelSight label instead of toggle
org_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
org_frame.pack(fill="x", padx=10, pady=5)
org_label = ctk.CTkLabel(
org_frame,
text="🏢 IntelSight Organization",
font=FONTS['body'],
text_color=COLORS['accent_primary']
)
org_label.pack()
# Search box
self.search_var = ctk.StringVar()
self.search_var.trace('w', self._on_search_changed)
search_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
search_frame.pack(fill="x", padx=10, pady=5)
self.search_entry = ctk.CTkEntry(
search_frame,
placeholder_text="Repository suchen...",
textvariable=self.search_var,
fg_color=COLORS['bg_primary'],
text_color=COLORS['text_primary']
)
self.search_entry.pack(fill="x")
# Repository list with scrollbar
list_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
list_frame.pack(fill="both", expand=True, padx=10, pady=(5, 10))
# Scrollable frame for repositories
self.scroll_frame = ctk.CTkScrollableFrame(
list_frame,
fg_color=COLORS['bg_primary'],
corner_radius=6
)
self.scroll_frame.pack(fill="both", expand=True)
# Repository items container
self.repo_container = ctk.CTkFrame(self.scroll_frame, fg_color=COLORS['bg_primary'])
self.repo_container.pack(fill="both", expand=True)
# Status label
self.status_label = ctk.CTkLabel(
self,
text="Lade Repositories...",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.status_label.pack(pady=5)
def _get_clone_directory(self) -> Path:
"""Get clone directory from settings or use default"""
try:
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
clone_dir = settings.get("gitea_clone_directory")
if clone_dir:
return Path(clone_dir)
except Exception as e:
logger.warning(f"Could not load clone directory from settings: {e}")
# Return default if not found in settings
return Path.home() / "GiteaRepos"
def refresh_repositories(self):
"""Refresh repository list from Gitea - only IntelSight"""
self.status_label.configure(text="Lade IntelSight Repositories...")
def fetch_repos():
try:
# Always fetch IntelSight organization repositories
all_repos = []
page = 1
while True:
# Fetch page by page to ensure we get all repositories
repos = self.repo_manager.list_organization_repositories("IntelSight", page=page, per_page=50)
if not repos:
break
all_repos.extend(repos)
page += 1
# Break if we got less than the requested amount (last page)
if len(repos) < 50:
break
self.repositories = all_repos
logger.info(f"Fetched {len(self.repositories)} repositories from IntelSight")
self.after(0, self._update_repository_list)
self.after(0, lambda: self.status_label.configure(
text=f"{len(self.repositories)} IntelSight Repositories"
))
except Exception as e:
logger.error(f"Error fetching repositories: {str(e)}")
self.after(0, lambda: self.status_label.configure(
text=f"Fehler: {str(e)}",
text_color=COLORS['accent_error']
))
threading.Thread(target=fetch_repos, daemon=True).start()
def _update_repository_list(self):
"""Update the displayed repository list"""
# Filter repositories based on search and remove duplicates
search_term = self.search_var.get().lower()
# Use a dict to track unique repos by name (preferring non-forks)
unique_repos = {}
for repo in self.repositories:
repo_name = repo['name']
# If we haven't seen this repo, or this one is not a fork (prefer originals)
if repo_name not in unique_repos or not repo.get('fork', False):
unique_repos[repo_name] = repo
# Filter based on search
filtered_repos = [
repo for repo in unique_repos.values()
if search_term in repo['name'].lower() or
search_term in repo.get('description', '').lower()
]
# Sort by name
filtered_repos.sort(key=lambda r: r['name'].lower())
# Get existing repo widgets
existing_widgets = {}
for widget in self.repo_container.winfo_children():
if hasattr(widget, 'repo_data'):
existing_widgets[widget.repo_data['name']] = widget
# Remove widgets for repos no longer in filtered list
filtered_names = {repo['name'] for repo in filtered_repos}
for name, widget in existing_widgets.items():
if name not in filtered_names:
widget.destroy()
# Update or create repository items
for i, repo in enumerate(filtered_repos):
if repo['name'] in existing_widgets:
# Update existing widget
widget = existing_widgets[repo['name']]
self._update_repo_item(widget, repo)
# Ensure correct order
widget.pack_forget()
widget.pack(fill="x", pady=2)
else:
# Create new widget
self._create_repo_item(repo)
def _create_repo_item(self, repo: Dict):
"""Create a repository item widget"""
# Main frame
item_frame = ctk.CTkFrame(
self.repo_container,
fg_color=COLORS['bg_gitea_tile'],
corner_radius=6,
height=70
)
item_frame.pack(fill="x", pady=2)
item_frame.pack_propagate(False)
# Store repo data
item_frame.repo_data = repo
repo['_widget'] = item_frame
# Content frame
content_frame = ctk.CTkFrame(item_frame, fg_color="transparent")
content_frame.pack(fill="both", expand=True, padx=10, pady=8)
# Top row - name and status
top_row = ctk.CTkFrame(content_frame, fg_color="transparent")
top_row.pack(fill="x")
# Repository name
name_label = ctk.CTkLabel(
top_row,
text=repo['name'],
font=FONTS['body'],
text_color=COLORS['text_primary'],
anchor="w"
)
name_label.pack(side="left", fill="x", expand=True)
# Clone/Status indicator
clone_dir = self._get_clone_directory()
local_path = clone_dir / repo['name']
if local_path.exists():
status_label = ctk.CTkLabel(
top_row,
text="✓ Cloned",
font=FONTS['small'],
text_color=COLORS['accent_success']
)
status_label.pack(side="right", padx=(5, 0))
# Check if it's a CPM project
if self.main_window and hasattr(self.main_window, 'project_manager'):
for project_data in self.main_window.project_manager.projects:
# project_data is a Project object, check if it has gitea_repo attribute
if hasattr(project_data, 'gitea_repo') and project_data.gitea_repo == f"{self.organization_name}/{repo['name']}":
cpm_label = ctk.CTkLabel(
top_row,
text="📁 CPM",
font=FONTS['small'],
text_color=COLORS['accent_primary']
)
cpm_label.pack(side="right", padx=(5, 0))
break
# Description
if repo.get('description'):
desc_label = ctk.CTkLabel(
content_frame,
text=repo['description'][:60] + '...' if len(repo.get('description', '')) > 60 else repo['description'],
font=FONTS['small'],
text_color=COLORS['text_secondary'],
anchor="w"
)
desc_label.pack(fill="x", pady=(2, 0))
# Bottom row - stats
stats_row = ctk.CTkFrame(content_frame, fg_color="transparent")
stats_row.pack(fill="x", pady=(4, 0))
# Private/Public
visibility = "🔒 Private" if repo.get('private', False) else "🌍 Public"
vis_label = ctk.CTkLabel(
stats_row,
text=visibility,
font=FONTS['small'],
text_color=COLORS['text_dim']
)
vis_label.pack(side="left", padx=(0, 10))
# Stars if available
if 'stars_count' in repo:
star_label = ctk.CTkLabel(
stats_row,
text=f"{repo['stars_count']}",
font=FONTS['small'],
text_color=COLORS['text_dim']
)
star_label.pack(side="left", padx=(0, 10))
# Menu button
menu_btn = ctk.CTkButton(
stats_row,
text="",
width=30,
height=24,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
font=('Segoe UI', 16)
)
menu_btn.configure(command=lambda: self._show_gitea_menu(repo, menu_btn))
menu_btn.pack(side="right")
# Click to select
def on_click(event):
self._select_repository(repo)
self._bind_click_recursive(item_frame, on_click)
# Hover effect
def on_enter(e):
if self.selected_repo != repo:
item_frame.configure(fg_color=COLORS['bg_gitea_hover'])
def on_leave(e):
if self.selected_repo != repo:
item_frame.configure(fg_color=COLORS['bg_gitea_tile'])
item_frame.bind("<Enter>", on_enter)
item_frame.bind("<Leave>", on_leave)
def _update_repo_item(self, item_frame: ctk.CTkFrame, repo: Dict):
"""Update an existing repository item widget"""
# Update stored data
item_frame.repo_data = repo
repo['_widget'] = item_frame
# Update clone status
local_path = Path.home() / "GiteaRepos" / repo['name']
# Find status label in widget hierarchy
for widget in item_frame.winfo_children():
if isinstance(widget, ctk.CTkFrame):
for child in widget.winfo_children():
if isinstance(child, ctk.CTkFrame):
for subchild in child.winfo_children():
if isinstance(subchild, ctk.CTkLabel) and "✓ Cloned" in subchild.cget("text"):
if not local_path.exists():
subchild.destroy()
break
def _select_repository(self, repo: Dict):
"""Select a repository"""
# Deselect previous
if self.selected_repo and '_widget' in self.selected_repo:
self.selected_repo['_widget'].configure(
fg_color=COLORS['bg_gitea_tile']
)
# Select new
self.selected_repo = repo
if '_widget' in repo:
repo['_widget'].configure(
fg_color=COLORS['bg_selected']
)
# Notify callback
if self.on_repo_select:
self.on_repo_select(repo)
def _on_search_changed(self, *args):
"""Handle search text change"""
self._update_repository_list()
def update_theme(self):
"""Update colors when theme changes"""
# Import updated colors
from gui.styles import get_colors, FONTS
global COLORS
COLORS = get_colors()
# Update main frame
self.configure(fg_color=COLORS['bg_secondary'])
# Update labels
self.title_label.configure(text_color=COLORS['text_primary'])
# Update search entry
self.search_entry.configure(
fg_color=COLORS['bg_primary'],
text_color=COLORS['text_primary'],
border_color=COLORS['border_primary']
)
# Update scroll frame and container
self.scroll_frame.configure(fg_color=COLORS['bg_primary'])
self.repo_container.configure(fg_color=COLORS['bg_primary'])
# Update status label
self.status_label.configure(text_color=COLORS['text_secondary'])
# Refresh the repository list to update item colors
self._update_repository_list()
# Clear any previous selection when theme changes
if self.selected_repo and '_widget' in self.selected_repo:
self.selected_repo['_widget'].configure(fg_color=COLORS['bg_gitea_tile'])
self.selected_repo = None
def _clone_repository(self, repo: Dict):
"""Clone repository and create CPM project"""
if self.main_window and hasattr(self.main_window, 'clone_repository'):
self.main_window.clone_repository(repo)
def _open_in_explorer(self, repo: Dict):
"""Open repository folder in file explorer"""
import platform
import subprocess
local_path = Path.home() / "GiteaRepos" / repo['name']
if local_path.exists():
if platform.system() == 'Windows':
os.startfile(str(local_path))
elif platform.system() == 'Darwin':
subprocess.Popen(['open', str(local_path)])
else:
subprocess.Popen(['xdg-open', str(local_path)])
def _git_status(self, repo: Dict):
"""Show git status for repository"""
if self.main_window and hasattr(self.main_window, 'show_git_status'):
# Create a pseudo-project object for compatibility
from types import SimpleNamespace
project = SimpleNamespace()
project.name = repo['name']
project.path = str(Path.home() / "GiteaRepos" / repo['name'])
self.main_window.show_git_status(project)
def _git_pull(self, repo: Dict):
"""Pull changes from remote"""
if self.main_window and hasattr(self.main_window, 'pull_from_gitea'):
from types import SimpleNamespace
project = SimpleNamespace()
project.name = repo['name']
project.path = str(Path.home() / "GiteaRepos" / repo['name'])
self.main_window.pull_from_gitea(project)
def _git_fetch(self, repo: Dict):
"""Fetch changes from remote"""
if self.main_window and hasattr(self.main_window, 'fetch_from_gitea'):
self.main_window.fetch_from_gitea(repo)
def _delete_local_copy(self, repo: Dict):
"""Delete local repository copy"""
from tkinter import messagebox
local_path = Path.home() / "GiteaRepos" / repo['name']
if messagebox.askyesno("Lokale Kopie löschen",
f"Möchten Sie die lokale Kopie von '{repo['name']}' wirklich löschen?\n\n"
f"Pfad: {local_path}"):
try:
import shutil
shutil.rmtree(local_path)
messagebox.showinfo("Erfolg", f"Lokale Kopie von '{repo['name']}' wurde gelöscht.")
self.refresh_repositories()
except Exception as e:
messagebox.showerror("Fehler", f"Fehler beim Löschen: {str(e)}")
def _show_repo_info(self, repo: Dict):
"""Show repository information"""
from tkinter import messagebox
info = f"Repository: {repo['name']}\n"
info += f"Beschreibung: {repo.get('description', 'Keine Beschreibung')}\n"
info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n"
info += f"Erstellt: {repo.get('created_at', 'Unbekannt')}\n"
info += f"Aktualisiert: {repo.get('updated_at', 'Unbekannt')}\n"
messagebox.showinfo("Repository Info", info)
def _open_in_browser(self, repo: Dict):
"""Open repository in browser"""
import webbrowser
if 'html_url' in repo:
webbrowser.open(repo['html_url'])
def _bind_click_recursive(self, widget, callback):
"""Recursively bind click event to widget and all children"""
# Don't bind to buttons to avoid interfering with their functionality
if not isinstance(widget, ctk.CTkButton):
widget.bind("<Button-1>", callback)
for child in widget.winfo_children():
# Skip buttons
if not isinstance(child, ctk.CTkButton):
self._bind_click_recursive(child, callback)
def clear_selection(self):
"""Clear the current selection"""
if self.selected_repo and '_widget' in self.selected_repo:
# Remove border from selection
self.selected_repo['_widget'].configure(border_width=0)
self.selected_repo = None

319
gui/gitea_toolbar.py Normale Datei
Datei anzeigen

@ -0,0 +1,319 @@
"""
Gitea Toolbar for context-sensitive Git operations
"""
import customtkinter as ctk
from tkinter import messagebox
import threading
from pathlib import Path
from typing import Optional, Callable, Dict
from gui.styles import COLORS, FONTS
class GiteaToolbar(ctk.CTkFrame):
def __init__(self, parent, **kwargs):
super().__init__(parent, fg_color=COLORS['bg_secondary'], height=60, **kwargs)
self.pack_propagate(False)
# Callbacks
self.callbacks: Dict[str, Optional[Callable]] = {
'commit': None,
'push': None,
'pull': None,
'fetch': None,
'status': None,
'branch': None,
'link': None,
'clone': None,
'create_project': None
}
# Current context
self.current_context = None # 'local_project', 'gitea_repo', or None
self.current_item = None # Project or Repo object
# Animation
self.is_visible = False
self.target_height = 60
self.current_height = 0
# Create buttons container
self.button_container = ctk.CTkFrame(self, fg_color="transparent")
self.button_container.pack(fill="both", expand=True, padx=10, pady=8)
# Status label
self.status_label = ctk.CTkLabel(
self.button_container,
text="",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.status_label.pack(side="left", padx=(0, 20))
# Button groups
self.button_groups = {
'local_project': [],
'gitea_repo': []
}
self.setup_buttons()
def setup_buttons(self):
"""Setup all buttons for different contexts"""
# Buttons for local project context
local_buttons = [
("📊 Status", "status", "Git-Status anzeigen\nZeigt uncommittete Änderungen"),
("💾 Commit", "commit", "Änderungen speichern\nLokale Änderungen committen"),
("📤 Push", "push", "Zu Gitea hochladen\nCommits zum Server pushen"),
("📥 Pull", "pull", "Änderungen abrufen\nNeueste Änderungen vom Server"),
("🔗 Verknüpfen", "link", "Mit Gitea verknüpfen\nLokales Projekt mit Repository verbinden"),
("🌿 Branch", "branch", "Branch-Verwaltung\nBranches anzeigen und wechseln")
]
# Buttons for Gitea repo context
gitea_buttons = [
("📥 Clone", "clone", "Repository klonen\nLokale Kopie erstellen und Projekt anlegen"),
("🔄 Fetch", "fetch", "Updates prüfen\nNeueste Änderungen abrufen"),
("📊 Info", "status", "Repository-Info\nDetails zum Repository anzeigen")
]
# Create button frames
self.local_frame = ctk.CTkFrame(self.button_container, fg_color="transparent")
self.gitea_frame = ctk.CTkFrame(self.button_container, fg_color="transparent")
# Create local project buttons
for text, callback_name, tooltip in local_buttons:
btn = self.create_button(self.local_frame, text, callback_name, tooltip)
self.button_groups['local_project'].append(btn)
# Create Gitea repo buttons
for text, callback_name, tooltip in gitea_buttons:
btn = self.create_button(self.gitea_frame, text, callback_name, tooltip)
self.button_groups['gitea_repo'].append(btn)
def create_button(self, parent, text, callback_name, tooltip):
"""Create a button with tooltip"""
btn = ctk.CTkButton(
parent,
text=text,
command=lambda: self.execute_callback(callback_name),
width=120,
height=36,
fg_color=COLORS['accent_secondary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF",
font=FONTS['body']
)
btn.pack(side="left", padx=4)
# Create tooltip
self.create_tooltip(btn, tooltip)
return btn
def create_tooltip(self, widget, text):
"""Create a tooltip for a widget"""
def on_enter(event):
# Destroy any existing tooltip first
if hasattr(widget, 'tooltip') and widget.tooltip:
try:
widget.tooltip.destroy()
except:
pass
tooltip = ctk.CTkToplevel(widget)
tooltip.wm_overrideredirect(True)
tooltip.wm_geometry(f"+{event.x_root + 10}+{event.y_root + 10}")
label = ctk.CTkLabel(
tooltip,
text=text,
font=FONTS['small'],
fg_color=COLORS['bg_secondary'],
text_color=COLORS['text_primary'],
corner_radius=6,
padx=10,
pady=5
)
label.pack()
widget.tooltip = tooltip
def on_leave(event):
if hasattr(widget, 'tooltip') and widget.tooltip:
try:
widget.tooltip.destroy()
widget.tooltip = None
except:
pass
# Also destroy tooltip when button is clicked
def on_click(event):
if hasattr(widget, 'tooltip') and widget.tooltip:
try:
widget.tooltip.destroy()
widget.tooltip = None
except:
pass
widget.bind("<Enter>", on_enter)
widget.bind("<Leave>", on_leave)
widget.bind("<Button-1>", on_click, add=True)
def set_callback(self, name: str, callback: Callable):
"""Set a callback for a button"""
if name in self.callbacks:
self.callbacks[name] = callback
def execute_callback(self, name: str):
"""Execute a callback if set"""
if name in self.callbacks and self.callbacks[name]:
# Run in thread to avoid blocking UI
threading.Thread(
target=self.callbacks[name],
args=(self.current_item,),
daemon=True
).start()
def show(self):
"""Show the toolbar"""
if not self.is_visible:
self.animate_show()
def show_for_context(self, context: str, item=None, status_text: str = ""):
"""Show toolbar for specific context"""
if context not in ['local_project', 'gitea_repo']:
self.hide()
return
self.current_context = context
self.current_item = item
# Update status
self.status_label.configure(text=status_text)
# Hide all frames
self.local_frame.pack_forget()
self.gitea_frame.pack_forget()
# Show relevant frame
if context == 'local_project':
self.local_frame.pack(side="left", padx=10)
elif context == 'gitea_repo':
self.gitea_frame.pack(side="left", padx=10)
# Animate show
if not self.is_visible:
self.animate_show()
def hide(self):
"""Hide the toolbar"""
if self.is_visible:
self.animate_hide()
def animate_show(self):
"""Animate toolbar appearing"""
print("animate_show called")
self.is_visible = True
# Pack the toolbar
try:
# Simple approach: just pack after any header that exists
parent_children = self.master.winfo_children()
print(f"Parent has {len(parent_children)} children")
# Find the best position (after header, before content)
packed = False
for i, child in enumerate(parent_children):
# Look for a frame that might be the header
if isinstance(child, ctk.CTkFrame):
# Check if it's likely the header (has small height or contains title)
try:
if child.winfo_height() < 100 or any(
isinstance(w, ctk.CTkLabel) and "Claude Project Manager" in str(w.cget("text"))
for w in child.winfo_children()
):
print(f"Packing toolbar after header at position {i}")
self.pack(fill="x", after=child, pady=(0, 10))
packed = True
break
except:
pass
if not packed:
print("Packing toolbar at default position")
self.pack(fill="x", pady=(0, 10))
except Exception as e:
print(f"Error showing toolbar: {e}")
import traceback
traceback.print_exc()
self.pack(fill="x", pady=(0, 10))
# Skip animation for now - just show full height
self.configure(height=self.target_height)
print(f"Toolbar configured with height {self.target_height}")
def animate_hide(self):
"""Animate toolbar disappearing"""
self.is_visible = False
# Destroy all tooltips before hiding
for group in self.button_groups.values():
for btn in group:
if hasattr(btn, 'tooltip') and btn.tooltip:
try:
btn.tooltip.destroy()
btn.tooltip = None
except:
pass
# Skip animation for now - just hide immediately
self.pack_forget()
def _animate_height(self, start, end, duration=200, callback=None):
"""Animate height change"""
steps = 10
step_duration = duration // steps
step_size = (end - start) / steps
def animate_step(current_step):
if current_step <= steps:
new_height = start + (step_size * current_step)
self.configure(height=new_height)
self.after(step_duration, lambda: animate_step(current_step + 1))
elif callback:
callback()
animate_step(0)
def set_selected_repo(self, repo):
"""Set the selected repository and show appropriate buttons"""
self.show_for_context('gitea_repo', repo)
def update_button_states(self, states: Dict[str, bool]):
"""Update button enabled/disabled states"""
for context_buttons in self.button_groups.values():
for btn in context_buttons:
# Extract callback name from button text
btn_text = btn.cget("text")
for callback_name in self.callbacks:
if callback_name in states:
# Simple matching - could be improved
btn.configure(state="normal" if states[callback_name] else "disabled")
def refresh_colors(self):
"""Refresh the toolbar's colors"""
from gui.styles import COLORS
# Update frame color
self.configure(fg_color=COLORS['bg_secondary'])
# Update status label
self.status_label.configure(text_color=COLORS['text_secondary'])
# Update all buttons
for context_buttons in self.button_groups.values():
for btn in context_buttons:
btn.configure(
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF"
)

16
gui/handlers/__init__.py Normale Datei
Datei anzeigen

@ -0,0 +1,16 @@
"""
Handler modules for MainWindow refactoring
Separates concerns and reduces God Class anti-pattern
"""
from .gitea_operations import GiteaOperationsHandler
from .process_manager import ProcessManagerHandler
from .project_manager import ProjectManagerHandler
from .ui_helpers import UIHelpersHandler
__all__ = [
'GiteaOperationsHandler',
'ProcessManagerHandler',
'ProjectManagerHandler',
'UIHelpersHandler'
]

19
gui/handlers/base_handler.py Normale Datei
Datei anzeigen

@ -0,0 +1,19 @@
"""
Base Handler for common functionality
"""
from typing import TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
class BaseHandler:
"""Base class for all handlers"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
logger.info(f"{self.__class__.__name__} initialized")

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@ -0,0 +1,101 @@
"""
Process Manager Handler
Handles process monitoring and management operations
"""
from typing import Optional, TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
from project_manager import Project
class ProcessManagerHandler:
"""Handles all process management operations for MainWindow"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
self.process_manager = main_window.process_manager
self.process_tracker = main_window.process_tracker
self.project_manager = main_window.project_manager
logger.info("ProcessManagerHandler initialized")
def monitor_process(self, project: 'Project', process) -> None:
"""Monitor a process for a project"""
return self.main_window._original_monitor_process(project, process)
def check_process_status(self) -> None:
"""Check status of all processes"""
return self.main_window._original_check_process_status()
def stop_project(self, project: 'Project') -> None:
"""Stop a running project"""
return self.main_window._original_stop_project(project)
def update_status(self, message: str, error: bool = False) -> None:
"""Update status bar message"""
# Direct implementation
from gui.styles import COLORS
if hasattr(self.main_window, 'status_label'):
self.main_window.status_label.configure(
text=message,
text_color=COLORS['accent_error'] if error else COLORS['text_secondary']
)
logger.debug(f"Status updated: {message} (error={error})")
def download_log(self) -> None:
"""Download comprehensive application log with all interactions"""
# Direct implementation
from tkinter import filedialog, messagebox
import os
from datetime import datetime
logger.info(f"Download log clicked - Total entries: {len(logger.log_entries)}")
try:
# Generate default filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_filename = f"CPM_FullLog_{timestamp}.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,
title="Save Complete Application Log"
)
if file_path:
# Export logs to chosen location with system info
logger.export_logs(file_path, include_system_info=True)
# Add completion entry
logger.info(f"Log export completed - Total entries: {len(logger.log_entries)}, Interactions: {logger.interaction_count}")
# Update status
self.update_status(f"Log saved: {os.path.basename(file_path)} ({len(logger.log_entries)} entries)")
# Show success message with log details
messagebox.showinfo(
"Log Export Successful",
f"Complete application log saved to:\n{file_path}\n\n"
f"Total log entries: {len(logger.log_entries):,}\n"
f"UI interactions logged: {logger.interaction_count:,}\n"
f"File size: {os.path.getsize(file_path) / 1024:.1f} KB"
)
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.log_exception(e, "download_log")
messagebox.showerror("Export Error", error_msg)
def _handle_process_ended(self, project: 'Project') -> None:
"""Handle process end event (private method)"""
return self.main_window._original_handle_process_ended(project)

Datei anzeigen

@ -0,0 +1,102 @@
"""
Project Manager Handler
Handles project CRUD operations and project-related UI updates
"""
from typing import Optional, TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
from project_manager import Project
class ProjectManagerHandler:
"""Handles all project management operations for MainWindow"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
self.project_manager = main_window.project_manager
self.terminal_launcher = main_window.terminal_launcher
self.readme_generator = main_window.readme_generator
self.vps_connection = main_window.vps_connection
logger.info("ProjectManagerHandler initialized")
def add_new_project(self) -> None:
"""Add a new project with logging"""
logger.info("Add new project initiated")
return self.main_window._original_add_new_project()
def open_project(self, project: 'Project') -> None:
"""Open a project with comprehensive logging"""
logger.info(f"Opening project: {project.name} (ID: {project.id}) at {project.path}")
return self.main_window._original_open_project(project)
def delete_project(self, project: 'Project') -> None:
"""Delete a project"""
# Direct implementation
from tkinter import messagebox
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.main_window.refresh_projects()
if hasattr(self.main_window, 'update_status'):
self.main_window.update_status(f"Removed: {project.name}")
logger.info(f"Project deleted: {project.name}")
def rename_project(self, project: 'Project') -> None:
"""Rename a project with logging"""
logger.info(f"Rename project initiated for: {project.name}")
return self.main_window._original_rename_project(project)
def refresh_projects(self) -> None:
"""Refresh the project display with logging"""
logger.debug("Refresh projects called")
logger.info(f"Refreshing projects - Total: {len(self.project_manager.projects)}")
return self.main_window._original_refresh_projects()
def create_project_from_repo(self, repo_data: dict, project_path: str) -> None:
"""Create a project from repository data"""
return self.main_window._original_create_project_from_repo(repo_data, project_path)
def open_vps_connection(self, project: 'Project') -> None:
"""Open VPS connection for project"""
return self.main_window._original_open_vps_connection(project)
def open_admin_panel(self, project: 'Project') -> None:
"""Open admin panel for project"""
return self.main_window._original_open_admin_panel(project)
def open_vps_docker(self, project: 'Project') -> None:
"""Open VPS Docker management"""
return self.main_window._original_open_vps_docker(project)
def open_readme(self, project: 'Project') -> None:
"""Open or generate README for project"""
return self.main_window._original_open_readme(project)
def generate_readme_background(self, project: 'Project') -> None:
"""Generate README in background"""
return self.main_window._original_generate_readme_background(project)
def open_gitea_window(self) -> None:
"""Open Gitea explorer window"""
return self.main_window._original_open_gitea_window()
def on_gitea_repo_select(self, repo_data: dict) -> None:
"""Handle Gitea repository selection"""
return self.main_window._original_on_gitea_repo_select(repo_data)
def clear_project_selection(self) -> None:
"""Clear current project selection"""
return self.main_window._original_clear_project_selection()
def on_project_select(self, project: 'Project', tile) -> None:
"""Handle project selection with logging"""
logger.info(f"Project selected: {project.name} (has Gitea repo: {bool(getattr(project, 'gitea_repo', None))})")
return self.main_window._original_on_project_select(project, tile)

203
gui/handlers/ui_helpers.py Normale Datei
Datei anzeigen

@ -0,0 +1,203 @@
"""
UI Helpers Handler
Handles UI creation, updates, and helper functions
"""
from typing import TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
from project_manager import Project
class UIHelpersHandler:
"""Handles UI helper operations for MainWindow"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
logger.info("UIHelpersHandler initialized")
def setup_ui(self) -> None:
"""Setup the main UI"""
return self.main_window._original_setup_ui()
def create_header(self) -> None:
"""Create header section"""
# Direct implementation with proper references
import customtkinter as ctk
from gui.styles import COLORS, FONTS
header_frame = ctk.CTkFrame(self.main_window.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.main_window.title_label = ctk.CTkLabel(
header_frame,
text="IntelSight - Claude Project Manager",
font=FONTS['heading'],
text_color=COLORS['text_primary']
)
self.main_window.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)
# Log download button
self.main_window.log_btn = ctk.CTkButton(
toolbar,
text="📥 Log",
command=self.main_window.download_log,
width=80,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF",
font=('Segoe UI', 12)
)
self.main_window.log_btn.pack(side="left", padx=(0, 10))
# Refresh button
self.main_window.refresh_btn = ctk.CTkButton(
toolbar,
text="↻ Refresh",
command=self.main_window.refresh_projects,
width=100,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary']
)
self.main_window.refresh_btn.pack(side="left", padx=(0, 10))
logger.debug("Header created")
def create_content_area(self) -> None:
"""Create content area"""
return self.main_window._original_create_content_area()
def create_status_bar(self) -> None:
"""Create status bar"""
# Direct implementation
import customtkinter as ctk
from gui.styles import COLORS, FONTS
self.main_window.status_bar = ctk.CTkFrame(
self.main_window.main_container,
fg_color=COLORS['bg_secondary'],
height=30
)
self.main_window.status_bar.pack(fill="x", side="bottom")
self.main_window.status_bar.pack_propagate(False)
self.main_window.status_label = ctk.CTkLabel(
self.main_window.status_bar,
text="Ready",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.main_window.status_label.pack(side="left", padx=20, pady=5)
# Project count
self.main_window.count_label = ctk.CTkLabel(
self.main_window.status_bar,
text="0 projects",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.main_window.count_label.pack(side="right", padx=20, pady=5)
logger.debug("Status bar created")
def create_project_tile(self, project: 'Project', parent) -> 'ProjectTile':
"""Create a project tile"""
return self.main_window._original_create_project_tile(project, parent)
def create_add_tile(self, parent) -> 'AddProjectTile':
"""Create add project tile"""
return self.main_window._original_create_add_tile(parent)
def create_project_tile_flow(self, project: 'Project', parent) -> 'ProjectTile':
"""Create project tile for flow layout"""
return self.main_window._original_create_project_tile_flow(project, parent)
def create_add_tile_flow(self, parent) -> 'AddProjectTile':
"""Create add tile for flow layout"""
return self.main_window._original_create_add_tile_flow(parent)
def refresh_ui(self) -> None:
"""Refresh the UI"""
return self.main_window._original_refresh_ui()
def load_and_apply_theme(self) -> None:
"""Load and apply theme preference"""
# Direct implementation - very simple method
import customtkinter as ctk
ctk.set_appearance_mode('dark')
logger.info("Theme applied: dark mode")
def on_window_resize(self, event) -> None:
"""Handle window resize event"""
# Direct implementation
# Only process resize events from the main window
if event.widget == self.root:
# Cancel previous timer
if self.main_window.resize_timer:
self.root.after_cancel(self.main_window.resize_timer)
# Set new timer to refresh after resize stops with differential update
self.main_window.resize_timer = self.root.after(
300,
lambda: self.main_window.refresh_projects(differential=True)
)
logger.debug("Window resize event handled")
def setup_interaction_tracking(self) -> None:
"""Setup interaction tracking"""
return self.main_window._original_setup_interaction_tracking()
def _show_scrollable_info(self, title: str, content: str) -> None:
"""Show scrollable information dialog"""
# Direct implementation - standalone UI method
import customtkinter as ctk
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()
logger.debug(f"Scrollable dialog shown: {title}")
def _differential_update(self, current_projects: list, previous_projects: list) -> None:
"""Perform differential update of projects"""
return self.main_window._original_differential_update(current_projects, previous_projects)
def _update_project_tiles_colors(self) -> None:
"""Update project tile colors"""
return self.main_window._original_update_project_tiles_colors()

3374
gui/main_window.py Normale Datei

Datei-Diff unterdrückt, da er zu groß ist Diff laden

272
gui/progress_bar.py Normale Datei
Datei anzeigen

@ -0,0 +1,272 @@
"""
Progress Bar Component for Git Operations
Provides visual feedback for long-running operations
"""
import customtkinter as ctk
from typing import Optional, Callable
import threading
import time
class ProgressBar(ctk.CTkToplevel):
"""Modern progress bar window for Git operations"""
def __init__(self, parent, title: str = "Operation in Progress", width: int = 500):
super().__init__(parent)
# Window configuration
self.title(title)
self.geometry(f"{width}x200")
self.resizable(False, False)
# Center on parent window
self.transient(parent)
self.grab_set()
# Colors from styles
from gui.styles import get_colors
self.colors = get_colors()
# Configure window
self.configure(fg_color=self.colors['bg_primary'])
# Create UI elements
self._setup_ui()
# Variables
self.is_cancelled = False
self._update_callback: Optional[Callable] = None
# Center window on parent
self._center_on_parent(parent)
def _setup_ui(self):
"""Create the progress bar UI"""
# Main container
self.container = ctk.CTkFrame(
self,
fg_color=self.colors['bg_secondary'],
corner_radius=10
)
self.container.pack(fill="both", expand=True, padx=20, pady=20)
# Title label
self.title_label = ctk.CTkLabel(
self.container,
text="",
font=("Segoe UI", 16, "bold"),
text_color=self.colors['text_primary']
)
self.title_label.pack(pady=(10, 5))
# Status label
self.status_label = ctk.CTkLabel(
self.container,
text="Initialisierung...",
font=("Segoe UI", 14),
text_color=self.colors['text_secondary']
)
self.status_label.pack(pady=(0, 15))
# Progress bar
self.progress = ctk.CTkProgressBar(
self.container,
width=440,
height=20,
corner_radius=10,
fg_color=self.colors['bg_tile'],
progress_color=self.colors['accent_success'],
mode="determinate"
)
self.progress.pack(pady=10)
self.progress.set(0)
# Percentage label
self.percentage_label = ctk.CTkLabel(
self.container,
text="0%",
font=("Segoe UI", 12),
text_color=self.colors['text_secondary']
)
self.percentage_label.pack(pady=(0, 10))
# Cancel button
self.cancel_button = ctk.CTkButton(
self.container,
text="Abbrechen",
width=100,
height=32,
corner_radius=6,
fg_color=self.colors['accent_error'],
hover_color="#FF5252",
text_color="#FFFFFF",
font=("Segoe UI", 13, "bold"),
command=self._cancel_operation
)
self.cancel_button.pack(pady=5)
def _center_on_parent(self, parent):
"""Center the window on parent"""
self.update_idletasks()
# Get parent position and size
parent_x = parent.winfo_x()
parent_y = parent.winfo_y()
parent_width = parent.winfo_width()
parent_height = parent.winfo_height()
# Get this window size
width = self.winfo_width()
height = self.winfo_height()
# Calculate position
x = parent_x + (parent_width - width) // 2
y = parent_y + (parent_height - height) // 2
# Set position
self.geometry(f"+{x}+{y}")
def _cancel_operation(self):
"""Handle cancel button click"""
self.is_cancelled = True
self.cancel_button.configure(state="disabled", text="Wird abgebrochen...")
def update_progress(self, value: float, status: str, title: str = "", is_error: bool = False):
"""Update progress bar
Args:
value: Progress value (0.0 to 1.0)
status: Status message to display
title: Optional title update
is_error: If True, colors the progress bar red
"""
# Ensure value is in range
value = max(0.0, min(1.0, value))
# Update UI in main thread
self.after(0, self._update_ui, value, status, title, is_error)
def _update_ui(self, value: float, status: str, title: str, is_error: bool = False):
"""Update UI elements (must be called from main thread)"""
if title:
self.title_label.configure(text=title)
self.status_label.configure(text=status)
self.progress.set(value)
self.percentage_label.configure(text=f"{int(value * 100)}%")
# Change color if error
if is_error:
self.progress.configure(progress_color=self.colors['accent_error'])
self.status_label.configure(text_color=self.colors['accent_error'])
# If complete, update button
if value >= 1.0 and not is_error:
self.cancel_button.configure(
text="Schließen",
state="normal",
command=self.destroy
)
# Auto-close after 0.5 seconds if successful
if not self.is_cancelled:
self.after(500, self.destroy)
def set_update_callback(self, callback: Callable):
"""Set callback for progress updates"""
self._update_callback = callback
def simulate_progress(self, steps: list):
"""Simulate progress for testing
Args:
steps: List of (duration, status, progress) tuples
"""
def run_simulation():
for duration, status, progress in steps:
if self.is_cancelled:
break
self.update_progress(progress, status)
time.sleep(duration)
if self._update_callback:
self._update_callback(progress, status)
thread = threading.Thread(target=run_simulation, daemon=True)
thread.start()
def set_error(self, error_message: str):
"""Set error state and message"""
self.update_progress(1.0, error_message, is_error=True)
self.cancel_button.configure(
text="OK",
state="normal",
command=self.destroy,
fg_color=self.colors['accent_error']
)
class GitOperationProgress:
"""Helper class to manage progress for Git operations"""
# Progress stages for different operations
CLONE_STAGES = [
(0.0, "Verbindung zum Server wird hergestellt..."),
(0.15, "Repository-Informationen werden abgerufen..."),
(0.30, "Objekte werden gezählt..."),
(0.50, "Objekte werden heruntergeladen..."),
(0.80, "Dateien werden entpackt..."),
(0.95, "Arbeitsverzeichnis wird erstellt..."),
(1.0, "Repository erfolgreich geklont!")
]
PUSH_STAGES = [
(0.0, "Verbindung wird hergestellt..."),
(0.10, "Authentifizierung läuft..."),
(0.25, "Lokale Änderungen werden analysiert..."),
(0.40, "Daten werden komprimiert..."),
(0.60, "Objekte werden übertragen..."),
(0.85, "Remote-Repository wird aktualisiert..."),
(1.0, "Push erfolgreich abgeschlossen!")
]
PULL_STAGES = [
(0.0, "Verbindung zum Server wird hergestellt..."),
(0.15, "Neue Änderungen werden gesucht..."),
(0.35, "Änderungen werden heruntergeladen..."),
(0.60, "Dateien werden entpackt..."),
(0.80, "Änderungen werden zusammengeführt..."),
(0.95, "Arbeitsverzeichnis wird aktualisiert..."),
(1.0, "Pull erfolgreich abgeschlossen!")
]
FETCH_STAGES = [
(0.0, "Verbindung wird hergestellt..."),
(0.20, "Remote-Referenzen werden abgerufen..."),
(0.40, "Neue Objekte werden gesucht..."),
(0.70, "Objekte werden heruntergeladen..."),
(0.90, "Lokale Referenzen werden aktualisiert..."),
(1.0, "Fetch erfolgreich abgeschlossen!")
]
COMMIT_STAGES = [
(0.0, "Änderungen werden vorbereitet..."),
(0.20, "Dateistatus wird geprüft..."),
(0.40, "Änderungen werden indiziert..."),
(0.60, "Commit-Objekt wird erstellt..."),
(0.80, "Referenzen werden aktualisiert..."),
(1.0, "Commit erfolgreich erstellt!")
]
@staticmethod
def get_stages(operation: str) -> list:
"""Get progress stages for an operation"""
stages_map = {
'clone': GitOperationProgress.CLONE_STAGES,
'push': GitOperationProgress.PUSH_STAGES,
'pull': GitOperationProgress.PULL_STAGES,
'fetch': GitOperationProgress.FETCH_STAGES,
'commit': GitOperationProgress.COMMIT_STAGES
}
return stages_map.get(operation.lower(), [])

1059
gui/project_tile.py Normale Datei

Datei-Diff unterdrückt, da er zu groß ist Diff laden

696
gui/settings_dialog.py Normale Datei
Datei anzeigen

@ -0,0 +1,696 @@
"""
Settings Dialog for the application
Allows users to configure various settings including view modes
"""
import customtkinter as ctk
from gui.styles import COLORS, FONTS
from utils.logger import logger
import json
from pathlib import Path
from tkinter import filedialog
import os
class SettingsDialog(ctk.CTkToplevel):
def __init__(self, parent, sidebar_view):
super().__init__(parent)
self.sidebar_view = sidebar_view
self.settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
# Window setup
self.title("Einstellungen")
self.minsize(650, 550)
self.resizable(True, True)
# Make modal
self.transient(parent)
self.grab_set()
# Load current settings
self.settings = self.load_settings()
# Setup UI
self.setup_ui()
# Update window size to fit content
self.update_idletasks()
# Center window
self.center_window()
# Focus
self.focus()
def setup_ui(self):
"""Setup the settings UI"""
# Main container
main_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_primary'])
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# Title
title_label = ctk.CTkLabel(
main_frame,
text="⚙️ Einstellungen",
font=FONTS['subtitle'],
text_color=COLORS['text_primary']
)
title_label.pack(pady=(0, 20))
# Tab view
self.tabview = ctk.CTkTabview(
main_frame,
fg_color=COLORS['bg_secondary'],
segmented_button_fg_color=COLORS['bg_tile'],
segmented_button_selected_color=COLORS['accent_primary'],
segmented_button_selected_hover_color=COLORS['accent_hover'],
segmented_button_unselected_color=COLORS['bg_tile'],
segmented_button_unselected_hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary'],
text_color_disabled=COLORS['text_dim']
)
self.tabview.pack(fill="both", expand=True)
# Add tabs
self.tabview.add("Allgemein")
self.tabview.add("Gitea")
self.tabview.add("Team-Aktivität")
# Setup each tab
self.setup_general_tab()
self.setup_gitea_tab()
self.setup_activity_tab()
# Buttons (at bottom of main frame)
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
button_frame.pack(fill="x", pady=(20, 0))
# Cancel button (left side)
cancel_btn = ctk.CTkButton(
button_frame,
text="Abbrechen",
command=self.destroy,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary'],
width=100
)
cancel_btn.pack(side="left", padx=(0, 5))
# Apply button (right side)
apply_btn = ctk.CTkButton(
button_frame,
text="Anwenden",
command=self.apply_settings,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
width=100
)
apply_btn.pack(side="right", padx=(5, 0))
# Save button (right side, before Apply)
save_btn = ctk.CTkButton(
button_frame,
text="Speichern",
command=self.save_settings_only,
fg_color=COLORS['accent_secondary'],
hover_color=COLORS['accent_hover'],
text_color=COLORS['text_primary'],
width=100
)
save_btn.pack(side="right", padx=(5, 0))
def setup_general_tab(self):
"""Setup general settings tab"""
tab = self.tabview.tab("Allgemein")
# Container with padding
container = ctk.CTkFrame(tab, fg_color="transparent")
container.pack(fill="both", expand=True, padx=20, pady=20)
# Placeholder for future general settings
info_label = ctk.CTkLabel(
container,
text="Allgemeine Einstellungen werden hier angezeigt.\n(Aktuell keine verfügbar)",
font=FONTS['body'],
text_color=COLORS['text_dim']
)
info_label.pack(pady=50)
def setup_gitea_tab(self):
"""Setup Gitea settings tab"""
tab = self.tabview.tab("Gitea")
# Scrollable container
container = ctk.CTkScrollableFrame(tab, fg_color="transparent")
container.pack(fill="both", expand=True, padx=20, pady=20)
# Clone Directory Section
clone_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile'])
clone_section.pack(fill="x", pady=(0, 15))
clone_header = ctk.CTkLabel(
clone_section,
text="📁 Clone-Verzeichnis",
font=FONTS['body'],
text_color=COLORS['text_primary']
)
clone_header.pack(anchor="w", padx=15, pady=(10, 5))
# Clone directory path
clone_path_frame = ctk.CTkFrame(clone_section, fg_color="transparent")
clone_path_frame.pack(fill="x", padx=30, pady=(5, 10))
default_clone_dir = str(Path.home() / "GiteaRepos")
self.clone_dir_var = ctk.StringVar(value=self.settings.get("gitea_clone_directory", default_clone_dir))
self.clone_dir_entry = ctk.CTkEntry(
clone_path_frame,
textvariable=self.clone_dir_var,
width=350,
fg_color=COLORS['bg_primary']
)
self.clone_dir_entry.pack(side="left", padx=(0, 10))
browse_btn = ctk.CTkButton(
clone_path_frame,
text="Durchsuchen...",
command=self.browse_clone_directory,
fg_color=COLORS['bg_secondary'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary'],
width=100
)
browse_btn.pack(side="left")
# Info label
info_label = ctk.CTkLabel(
clone_section,
text="Standardverzeichnis für geklonte Repositories",
font=FONTS['small'],
text_color=COLORS['text_dim']
)
info_label.pack(anchor="w", padx=30, pady=(0, 10))
# Server Configuration Section
server_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile'])
server_section.pack(fill="x", pady=(0, 15))
server_header = ctk.CTkLabel(
server_section,
text="🌐 Gitea Server",
font=FONTS['body'],
text_color=COLORS['text_primary']
)
server_header.pack(anchor="w", padx=15, pady=(10, 5))
# Server URL
url_label = ctk.CTkLabel(
server_section,
text="Server URL:",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
url_label.pack(anchor="w", padx=30, pady=(5, 0))
self.gitea_url_var = ctk.StringVar(value=self.settings.get("gitea_server_url", "https://gitea-undso.intelsight.de"))
self.gitea_url_entry = ctk.CTkEntry(
server_section,
textvariable=self.gitea_url_var,
width=350,
fg_color=COLORS['bg_primary']
)
self.gitea_url_entry.pack(padx=30, pady=(0, 5))
# API Token
token_label = ctk.CTkLabel(
server_section,
text="API Token:",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
token_label.pack(anchor="w", padx=30, pady=(5, 0))
self.gitea_token_var = ctk.StringVar(value=self.settings.get("gitea_api_token", ""))
self.gitea_token_entry = ctk.CTkEntry(
server_section,
textvariable=self.gitea_token_var,
width=350,
fg_color=COLORS['bg_primary'],
show="*"
)
self.gitea_token_entry.pack(padx=30, pady=(0, 5))
# Username
user_label = ctk.CTkLabel(
server_section,
text="Benutzername:",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
user_label.pack(anchor="w", padx=30, pady=(5, 0))
self.gitea_user_var = ctk.StringVar(value=self.settings.get("gitea_username", ""))
self.gitea_user_entry = ctk.CTkEntry(
server_section,
textvariable=self.gitea_user_var,
width=350,
fg_color=COLORS['bg_primary']
)
self.gitea_user_entry.pack(padx=30, pady=(0, 10))
# Test connection button
test_btn = ctk.CTkButton(
server_section,
text="Verbindung testen",
command=self.test_gitea_connection,
fg_color=COLORS['bg_secondary'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary'],
width=150
)
test_btn.pack(pady=(5, 10))
# Status label
self.gitea_status_label = ctk.CTkLabel(
server_section,
text="",
font=FONTS['small'],
text_color=COLORS['text_secondary'],
height=20
)
self.gitea_status_label.pack(pady=(0, 10))
def setup_activity_tab(self):
"""Setup activity server settings tab"""
tab = self.tabview.tab("Team-Aktivität")
# Container with padding
container = ctk.CTkFrame(tab, fg_color="transparent")
container.pack(fill="both", expand=True, padx=20, pady=20)
# Activity Server Section
activity_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile'])
activity_section.pack(fill="x", pady=(0, 15))
activity_header = ctk.CTkLabel(
activity_section,
text="👥 Team-Aktivität Server",
font=FONTS['body'],
text_color=COLORS['text_primary']
)
activity_header.pack(anchor="w", padx=15, pady=(10, 5))
# Server URL
url_label = ctk.CTkLabel(
activity_section,
text="Server URL:",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
url_label.pack(anchor="w", padx=30, pady=(5, 0))
self.server_url_var = ctk.StringVar(value=self.settings.get("activity_server_url", "http://91.99.192.14:3001"))
self.server_url_entry = ctk.CTkEntry(
activity_section,
textvariable=self.server_url_var,
width=350,
fg_color=COLORS['bg_primary']
)
self.server_url_entry.pack(padx=30, pady=(0, 5))
# API Key
api_label = ctk.CTkLabel(
activity_section,
text="API Key:",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
api_label.pack(anchor="w", padx=30, pady=(5, 0))
self.api_key_var = ctk.StringVar(value=self.settings.get("activity_api_key", ""))
self.api_key_entry = ctk.CTkEntry(
activity_section,
textvariable=self.api_key_var,
width=350,
fg_color=COLORS['bg_primary'],
show="*"
)
self.api_key_entry.pack(padx=30, pady=(0, 5))
# User Name
user_label = ctk.CTkLabel(
activity_section,
text="Benutzername:",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
user_label.pack(anchor="w", padx=30, pady=(5, 0))
self.user_name_var = ctk.StringVar(value=self.settings.get("activity_user_name", ""))
self.user_name_entry = ctk.CTkEntry(
activity_section,
textvariable=self.user_name_var,
width=350,
fg_color=COLORS['bg_primary']
)
self.user_name_entry.pack(padx=30, pady=(0, 5))
# Test connection button
test_btn = ctk.CTkButton(
activity_section,
text="Verbindung testen",
command=self.test_activity_connection,
fg_color=COLORS['bg_secondary'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary'],
width=150
)
test_btn.pack(pady=(5, 10))
# Status label
self.connection_status_label = ctk.CTkLabel(
activity_section,
text="",
font=FONTS['small'],
text_color=COLORS['text_secondary'],
height=20
)
self.connection_status_label.pack(pady=(0, 10))
def load_settings(self):
"""Load settings from file"""
try:
if self.settings_file.exists():
with open(self.settings_file, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load UI settings: {e}")
return {} # No settings currently
def save_settings(self):
"""Save settings to file"""
try:
self.settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.settings_file, 'w') as f:
json.dump(self.settings, f, indent=2)
logger.info(f"Saved UI settings: {self.settings}")
except Exception as e:
logger.error(f"Failed to save UI settings: {e}")
def browse_clone_directory(self):
"""Browse for clone directory"""
current_dir = self.clone_dir_var.get()
if not os.path.exists(current_dir):
current_dir = str(Path.home())
directory = filedialog.askdirectory(
title="Clone-Verzeichnis auswählen",
initialdir=current_dir,
parent=self
)
if directory:
self.clone_dir_var.set(directory)
logger.info(f"Selected clone directory: {directory}")
def test_gitea_connection(self):
"""Test connection to Gitea server"""
import requests
server_url = self.gitea_url_var.get().strip()
api_token = self.gitea_token_var.get().strip()
if not server_url:
self.gitea_status_label.configure(
text="⚠️ Bitte Server URL eingeben",
text_color=COLORS['accent_warning']
)
return
self.gitea_status_label.configure(
text="🔄 Teste Verbindung...",
text_color=COLORS['text_secondary']
)
self.update()
try:
# Try to connect to the Gitea API
headers = {}
if api_token:
headers["Authorization"] = f"token {api_token}"
response = requests.get(
f"{server_url}/api/v1/version",
timeout=5,
headers=headers
)
if response.status_code == 200:
version = response.json().get('version', 'Unknown')
self.gitea_status_label.configure(
text=f"✅ Verbindung erfolgreich! (Gitea {version})",
text_color=COLORS['accent_success']
)
logger.info(f"Gitea connection successful: {server_url}, version: {version}")
else:
self.gitea_status_label.configure(
text=f"❌ Server antwortet mit Status {response.status_code}",
text_color=COLORS['accent_error']
)
logger.warning(f"Gitea server returned status {response.status_code}")
except requests.exceptions.ConnectionError:
self.gitea_status_label.configure(
text="❌ Server nicht erreichbar",
text_color=COLORS['accent_error']
)
logger.error(f"Gitea server not reachable: {server_url}")
except requests.exceptions.Timeout:
self.gitea_status_label.configure(
text="❌ Verbindung Timeout",
text_color=COLORS['accent_error']
)
logger.error(f"Gitea server connection timeout: {server_url}")
except Exception as e:
self.gitea_status_label.configure(
text=f"❌ Fehler: {str(e)}",
text_color=COLORS['accent_error']
)
logger.error(f"Gitea server connection error: {e}")
def save_settings_only(self):
"""Save settings without applying to service or closing dialog"""
# Get activity values
server_url = self.server_url_var.get().strip()
api_key = self.api_key_var.get().strip()
user_name = self.user_name_var.get().strip()
# Get Gitea values
gitea_url = self.gitea_url_var.get().strip()
gitea_token = self.gitea_token_var.get().strip()
gitea_user = self.gitea_user_var.get().strip()
clone_dir = self.clone_dir_var.get().strip()
# Update settings
self.settings["activity_server_url"] = server_url
self.settings["activity_api_key"] = api_key
self.settings["activity_user_name"] = user_name
self.settings["gitea_server_url"] = gitea_url
self.settings["gitea_api_token"] = gitea_token
self.settings["gitea_username"] = gitea_user
self.settings["gitea_clone_directory"] = clone_dir
self.save_settings()
# Show confirmation on the active tab
current_tab = self.tabview.get()
if current_tab == "Team-Aktivität":
self.connection_status_label.configure(
text="✅ Einstellungen gespeichert!",
text_color=COLORS['accent_success']
)
self.after(2000, lambda: self.connection_status_label.configure(text=""))
elif current_tab == "Gitea":
self.gitea_status_label.configure(
text="✅ Einstellungen gespeichert!",
text_color=COLORS['accent_success']
)
self.after(2000, lambda: self.gitea_status_label.configure(text=""))
logger.info("Settings saved successfully")
def apply_settings(self):
"""Apply the selected settings"""
import uuid
from services.activity_sync import activity_service
# Save all settings first
self.save_settings_only()
# Apply activity service settings
activity_service.server_url = self.settings.get("activity_server_url", "")
activity_service.api_key = self.settings.get("activity_api_key", "")
activity_service.user_name = self.settings.get("activity_user_name", "")
activity_service.user_id = self.settings.get("activity_user_id", str(uuid.uuid4()))
# Save user ID if newly generated
if "activity_user_id" not in self.settings:
self.settings["activity_user_id"] = activity_service.user_id
self.save_settings()
# Save activity settings
activity_service.save_settings()
# Reconnect if settings changed
if activity_service.connected:
activity_service.disconnect()
if activity_service.is_configured():
activity_service.connect()
logger.info("Activity server settings applied")
# Apply Gitea settings to config
self.update_gitea_config()
# Close dialog
self.destroy()
def update_gitea_config(self):
"""Update Gitea configuration in the application"""
try:
# Import here to avoid circular imports
from src.gitea.gitea_client import gitea_config
from src.gitea.git_operations import git_ops
# Update Gitea config
if hasattr(gitea_config, 'base_url'):
gitea_config.base_url = self.settings.get("gitea_server_url", gitea_config.base_url)
if hasattr(gitea_config, 'api_token'):
gitea_config.api_token = self.settings.get("gitea_api_token", gitea_config.api_token)
if hasattr(gitea_config, 'username'):
gitea_config.username = self.settings.get("gitea_username", gitea_config.username)
# Update clone directory
clone_dir = self.settings.get("gitea_clone_directory")
if clone_dir:
# Re-initialize git_ops to use new settings
from src.gitea.git_operations import init_git_ops
init_git_ops()
# Now update the clone directory
if hasattr(git_ops, 'default_clone_dir'):
git_ops.default_clone_dir = Path(clone_dir)
logger.info(f"Updated git_ops clone directory to: {clone_dir}")
# Update all existing RepositoryManager instances
# This ensures that any git_ops instances in the application get the new directory
try:
# Update in main window if it exists
parent = self.master
if hasattr(parent, 'gitea_handler') and hasattr(parent.gitea_handler, 'repo_manager'):
if hasattr(parent.gitea_handler.repo_manager, 'git_ops'):
parent.gitea_handler.repo_manager.git_ops.default_clone_dir = Path(clone_dir)
logger.info("Updated main window's git_ops clone directory")
# Update in sidebar gitea explorer if it exists
if hasattr(parent, 'sidebar') and hasattr(parent.sidebar, 'gitea_explorer'):
if hasattr(parent.sidebar.gitea_explorer, 'repo_manager'):
if hasattr(parent.sidebar.gitea_explorer.repo_manager, 'git_ops'):
parent.sidebar.gitea_explorer.repo_manager.git_ops.default_clone_dir = Path(clone_dir)
logger.info("Updated sidebar's git_ops clone directory")
except Exception as e:
logger.warning(f"Could not update all git_ops instances: {e}")
logger.info("Gitea configuration updated")
except ImportError as e:
logger.warning(f"Could not update Gitea config: {e}")
except Exception as e:
logger.error(f"Error updating Gitea config: {e}")
def test_activity_connection(self):
"""Test connection to activity server"""
import requests
from tkinter import messagebox
server_url = self.server_url_var.get().strip()
api_key = self.api_key_var.get().strip()
if not server_url:
self.connection_status_label.configure(
text="⚠️ Bitte Server URL eingeben",
text_color=COLORS['accent_warning']
)
return
self.connection_status_label.configure(
text="🔄 Teste Verbindung...",
text_color=COLORS['text_secondary']
)
self.update()
try:
# Try to connect to the server
response = requests.get(
f"{server_url}/health",
timeout=5,
headers={"Authorization": f"Bearer {api_key}"} if api_key else {}
)
if response.status_code == 200:
self.connection_status_label.configure(
text="✅ Verbindung erfolgreich!",
text_color=COLORS['accent_success']
)
logger.info(f"Activity server connection successful: {server_url}")
else:
self.connection_status_label.configure(
text=f"❌ Server antwortet mit Status {response.status_code}",
text_color=COLORS['accent_error']
)
logger.warning(f"Activity server returned status {response.status_code}")
except requests.exceptions.ConnectionError:
self.connection_status_label.configure(
text="❌ Server nicht erreichbar",
text_color=COLORS['accent_error']
)
logger.error(f"Activity server not reachable: {server_url}")
except requests.exceptions.Timeout:
self.connection_status_label.configure(
text="❌ Verbindung Timeout",
text_color=COLORS['accent_error']
)
logger.error(f"Activity server connection timeout: {server_url}")
except Exception as e:
self.connection_status_label.configure(
text=f"❌ Fehler: {str(e)}",
text_color=COLORS['accent_error']
)
logger.error(f"Activity server connection error: {e}")
def center_window(self):
"""Center the dialog on parent window"""
self.update_idletasks()
# Get parent window position
parent = self.master
parent_x = parent.winfo_x()
parent_y = parent.winfo_y()
parent_width = parent.winfo_width()
parent_height = parent.winfo_height()
# Get dialog size
dialog_width = self.winfo_width()
dialog_height = self.winfo_height()
# Calculate position
x = parent_x + (parent_width - dialog_width) // 2
y = parent_y + (parent_height - dialog_height) // 2
self.geometry(f"+{x}+{y}")

42
gui/sidebar_view.py Normale Datei
Datei anzeigen

@ -0,0 +1,42 @@
"""
Sidebar View Manager
Simplified version with only Gitea explorer
"""
import customtkinter as ctk
from typing import Optional
from gui.styles import COLORS, FONTS
from gui.gitea_explorer import GiteaExplorer
from utils.logger import logger
class SidebarView(ctk.CTkFrame):
def __init__(self, parent, on_repo_select=None, **kwargs):
super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs)
self.on_repo_select = on_repo_select
self.main_window = None # Will be set by main window
# Create Gitea explorer
self.gitea_explorer = GiteaExplorer(
self,
on_repo_select=self.on_repo_select
)
self.gitea_explorer.pack(fill="both", expand=True)
logger.info("Sidebar view initialized with Gitea explorer")
def get_gitea_explorer(self) -> Optional[GiteaExplorer]:
"""Get the Gitea explorer instance"""
return self.gitea_explorer
def refresh_all(self):
"""Refresh all explorers"""
if self.gitea_explorer:
self.gitea_explorer.refresh_repositories()
def set_main_window(self, main_window):
"""Set main window reference"""
self.main_window = main_window
if self.gitea_explorer:
self.gitea_explorer.main_window = main_window

124
gui/styles.py Normale Datei
Datei anzeigen

@ -0,0 +1,124 @@
"""
Style Configuration for GUI
Modern design with dark/light theme support
"""
# Dark Mode color definitions (only mode)
COLORS = {
# Background colors
'bg_primary': '#000000', # Schwarz
'bg_secondary': '#1A1F3A', # Dunkleres Blau
'bg_tile': '#232D53', # Dunkelblau
'bg_tile_hover': '#2A3560', # Helleres Dunkelblau
'bg_vps': '#1A2347', # VPS tile etwas dunkler
'bg_gitea_tile': '#2A3560', # Gitea tiles - deutlich heller für bessere Lesbarkeit
'bg_gitea_hover': '#364170', # Gitea hover - noch heller
'bg_selected': '#364170', # Selected - dunkleres Blau ohne Alpha
# Text colors
'text_primary': '#FFFFFF', # Weiß
'text_secondary': '#B0B0B0', # Grau
'text_dim': '#707070', # Dunkles Grau
# Accent colors
'accent_primary': '#00D4FF', # Cyan/Hellblau
'accent_hover': '#00E5FF', # Helleres Cyan
'accent_secondary': '#6B7AA1', # Sekundäres Blau-Grau
'accent_selected': '#00D4FF', # Auswahl-Farbe
'accent_success': '#4CAF50', # Success green
'accent_warning': '#FF9800', # Warning orange
'accent_error': '#F44336', # Error red
'accent_vps': '#00D4FF', # VPS auch Cyan
# Border colors
'border_primary': '#232D53', # Dunkelblau
'border_secondary': '#2A3560', # Helleres Dunkelblau
}
# Get current colors (always dark mode)
def get_colors():
"""Get colors (always returns dark mode colors)"""
return COLORS
# Font Configuration
FONTS = {
'heading': ('Segoe UI', 28, 'bold'),
'subheading': ('Segoe UI', 22, 'bold'),
'title': ('Segoe UI', 20, 'bold'), # Added for gitea UI
'subtitle': ('Segoe UI', 16, 'bold'), # Added for gitea UI
'tile_title': ('Segoe UI', 18, 'bold'),
'tile_text': ('Segoe UI', 14),
'body': ('Segoe UI', 14), # Added for gitea UI
'button': ('Segoe UI', 13, 'bold'),
'small': ('Segoe UI', 12),
'code': ('Consolas', 12), # Added for gitea UI code display
}
# Tile Dimensions
TILE_SIZE = {
'width': 280,
'height': 180,
'padding': 15,
'margin': 10,
}
# Button Styles
BUTTON_STYLES = {
'primary': {
'fg_color': COLORS['accent_primary'],
'hover_color': COLORS['accent_hover'],
'text_color': '#FFFFFF',
'corner_radius': 6,
'height': 32,
},
'secondary': {
'fg_color': COLORS['bg_secondary'],
'hover_color': COLORS['bg_tile'],
'text_color': COLORS['text_primary'],
'corner_radius': 6,
'height': 32,
'border_width': 1,
'border_color': COLORS['border_primary'],
},
'danger': {
'fg_color': COLORS['accent_error'],
'hover_color': '#FF5252',
'text_color': '#FFFFFF',
'corner_radius': 6,
'height': 32,
},
'vps': {
'fg_color': COLORS['accent_vps'],
'hover_color': COLORS['accent_hover'],
'text_color': '#FFFFFF',
'corner_radius': 6,
'height': 32,
}
}
def get_button_styles():
"""Get button styles (for compatibility)"""
return BUTTON_STYLES
# Window Configuration
WINDOW_CONFIG = {
'title': 'Claude Project Manager',
'width': 1600, # Increased to fit 4 VPS tiles side by side
'height': 1000, # Increased to show Gitea repos without scrolling
'min_width': 800,
'min_height': 600,
}
# Theme functions (kept for compatibility but always return dark)
def set_theme(theme_name):
"""Kept for compatibility - does nothing as only dark mode exists"""
return True
def get_theme():
"""Always returns 'dark'"""
return 'dark'
def toggle_theme():
"""Kept for compatibility - always returns 'dark'"""
return 'dark'

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.9 KiB

37
logo_header.svg Normale Datei
Datei anzeigen

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50" height="50" viewBox="0 0 100 115" xmlns="http://www.w3.org/2000/svg">
<!-- Shield and eye logo for header - resized -->
<g transform="translate(0, 0)">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="#FFFFFF"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="#FFFFFF"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="#FFFFFF"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="#FFFFFF"/>
</g>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.0 KiB

54
main.py Normale Datei
Datei anzeigen

@ -0,0 +1,54 @@
"""
Claude Project Manager
Main entry point for the application
"""
import sys
import os
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gui.main_window import MainWindow
from utils.logger import logger
def main():
"""Main application entry point"""
logger.info("Starting Claude Project Manager")
try:
# Import and set theme first
import json
from gui.styles import set_theme
import customtkinter as ctk
# Load theme preference before creating UI
settings_file = os.path.join(os.path.dirname(__file__), "data/settings.json")
theme = 'dark' # Default
try:
if os.path.exists(settings_file):
with open(settings_file, 'r') as f:
settings = json.load(f)
if 'theme' in settings:
theme = settings['theme']
except:
pass
# Set theme before UI creation
ctk.set_appearance_mode(theme)
set_theme(theme)
# Create and run the application
app = MainWindow()
logger.info("Application window created, starting main loop")
app.run()
except Exception as e:
logger.error(f"Error starting application: {e}")
print(f"Error starting application: {e}")
import traceback
traceback.print_exc()
input("Press Enter to exit...")
if __name__ == "__main__":
main()

108
manage_refactoring.py Normale Datei
Datei anzeigen

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
Refactoring Management Tool
Manage feature flags for the MainWindow refactoring
"""
import argparse
import json
from pathlib import Path
from gui.config import refactoring_config
def show_status():
"""Show current refactoring status"""
status = refactoring_config.get_status()
print("\n=== Refactoring Configuration Status ===")
print(f"Config file: {status['config_file']}")
print("\nFeature Flags:")
for flag, value in status['flags'].items():
status_str = "✅ ENABLED" if value else "❌ DISABLED"
print(f" {flag}: {status_str}")
print()
def enable_handler(handler_name):
"""Enable a specific handler"""
refactoring_config.enable_handler(handler_name)
refactoring_config.save_config()
print(f"✅ Enabled {handler_name} handler")
# Reload config to show updated status
from gui.config import RefactoringConfig
updated_config = RefactoringConfig()
status = updated_config.get_status()
print("\n=== Refactoring Configuration Status ===")
print(f"Config file: {status['config_file']}")
print("\nFeature Flags:")
for flag, value in status['flags'].items():
status_str = "✅ ENABLED" if value else "❌ DISABLED"
print(f" {flag}: {status_str}")
print()
def disable_all():
"""Disable all refactoring features"""
refactoring_config.disable_all()
refactoring_config.save_config()
print("❌ All refactoring features disabled")
show_status()
def set_flag(flag_name, value):
"""Set a specific flag"""
refactoring_config.set(flag_name, value)
refactoring_config.save_config()
status_str = "enabled" if value else "disabled"
print(f"✅ Flag {flag_name} {status_str}")
show_status()
def create_test_config():
"""Create a test configuration with gradual enablement"""
print("\n=== Creating Test Configuration ===")
# Phase 1: Enable UI helpers only (safest)
print("\nPhase 1: UI Helpers only")
refactoring_config.disable_all()
refactoring_config.set('USE_UI_HELPERS', True)
refactoring_config.save_config()
print("\nTest with: python main.py")
print("If stable, proceed to Phase 2")
def main():
parser = argparse.ArgumentParser(description='Manage MainWindow refactoring')
subparsers = parser.add_subparsers(dest='command', help='Commands')
# Status command
subparsers.add_parser('status', help='Show current configuration')
# Enable command
enable_parser = subparsers.add_parser('enable', help='Enable a handler')
enable_parser.add_argument('handler', choices=['gitea', 'process', 'project', 'ui'],
help='Handler to enable')
# Disable all command
subparsers.add_parser('disable-all', help='Disable all refactoring features')
# Set flag command
set_parser = subparsers.add_parser('set', help='Set a specific flag')
set_parser.add_argument('flag', help='Flag name')
set_parser.add_argument('value', choices=['true', 'false'], help='Flag value')
# Test config command
subparsers.add_parser('test-config', help='Create test configuration')
args = parser.parse_args()
if args.command == 'status' or not args.command:
show_status()
elif args.command == 'enable':
enable_handler(args.handler)
elif args.command == 'disable-all':
disable_all()
elif args.command == 'set':
value = args.value.lower() == 'true'
set_flag(args.flag, value)
elif args.command == 'test-config':
create_test_config()
else:
parser.print_help()
if __name__ == '__main__':
main()

145
process_manager.py Normale Datei
Datei anzeigen

@ -0,0 +1,145 @@
"""
Process Manager Module
Tracks and manages running Claude processes
"""
import subprocess
import os
import json
from typing import Dict, Optional
from datetime import datetime
from utils.logger import logger
class ProcessManager:
def __init__(self):
self.processes: Dict[str, subprocess.Popen] = {}
self.process_data_file = os.path.join(os.path.dirname(__file__), "data/running_processes.json")
logger.info("Initializing ProcessManager")
self.load_process_data()
def load_process_data(self):
"""Load process data from file"""
try:
if os.path.exists(self.process_data_file):
with open(self.process_data_file, 'r') as f:
data = json.load(f)
# Clean up old data (processes that are no longer running)
self.clean_process_data(data)
except Exception as e:
logger.error(f"Error loading process data: {e}")
print(f"Error loading process data: {e}")
def clean_process_data(self, data: dict):
"""Clean up process data for processes that are no longer running"""
# For now, we start fresh each time the app starts
# In a real app, we might check if processes are still running
self.save_process_data({})
def save_process_data(self, data: dict = None):
"""Save process data to file"""
if data is None:
data = {
project_id: {
"pid": proc.pid if proc else None,
"start_time": datetime.now().isoformat(),
"status": "running" if proc and proc.poll() is None else "stopped"
}
for project_id, proc in self.processes.items()
}
os.makedirs(os.path.dirname(self.process_data_file), exist_ok=True)
try:
with open(self.process_data_file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error saving process data: {e}")
def start_process(self, project_id: str, command: list, shell: bool = False) -> subprocess.Popen:
"""Start a new process for a project"""
logger.info(f"Starting process for project: {project_id}")
logger.debug(f"Command: {' '.join(command if isinstance(command, list) else [command])}")
# Stop existing process if any
if project_id in self.processes:
logger.warning(f"Stopping existing process for project: {project_id}")
self.stop_process(project_id)
# Start new process
process = subprocess.Popen(command, shell=shell)
self.processes[project_id] = process
logger.info(f"Process started with PID: {process.pid}")
self.save_process_data()
return process
def stop_process(self, project_id: str, project_name: str = None) -> bool:
"""Stop a process for a project"""
logger.info(f"Stopping process for project: {project_id} (name: {project_name})")
try:
if project_id == "vps-permanent":
# Kill VPS windows
cmd_kill_cmd = 'taskkill /fi "WINDOWTITLE eq Claude VPS Server" /f >nul 2>&1'
logger.debug(f"Executing VPS kill command: {cmd_kill_cmd}")
os.system(cmd_kill_cmd)
else:
# Kill project-specific window
if project_name:
window_title = f"Claude - {project_name}"
# Use powershell for more accurate window title matching
ps_cmd = f'powershell -Command "Get-Process | Where-Object {{$_.MainWindowTitle -eq \'{window_title}\'}} | Stop-Process -Force"'
logger.debug(f"Executing PowerShell kill command: {ps_cmd}")
os.system(ps_cmd)
# Also try taskkill as fallback
cmd_kill_cmd = f'taskkill /fi "WINDOWTITLE eq {window_title}" /f >nul 2>&1'
logger.debug(f"Executing taskkill command: {cmd_kill_cmd}")
os.system(cmd_kill_cmd)
# Clean up tracked process if exists
if project_id in self.processes:
process = self.processes[project_id]
if process and process.poll() is None:
try:
process.terminate()
process.wait(timeout=2)
except:
try:
process.kill()
except:
pass
del self.processes[project_id]
self.save_process_data()
logger.info(f"Process stopped successfully for project: {project_id}")
return True
except Exception as e:
logger.error(f"Error stopping process: {e}")
print(f"Error stopping process: {e}")
return False
def check_window_exists(self, window_title: str) -> bool:
"""Check if a window with the given title exists"""
# This method is not used anymore - checking is done in ProjectProcessTracker
return False
def is_running(self, project_id: str) -> bool:
"""Check if a process is running for a project"""
# Don't rely on the launcher process, check for actual window
return False # Will be overridden by is_project_running
def stop_all_processes(self):
"""Stop all running processes"""
project_ids = list(self.processes.keys())
for project_id in project_ids:
self.stop_process(project_id)
def get_process_info(self, project_id: str) -> Optional[dict]:
"""Get process information for a project"""
if project_id in self.processes:
process = self.processes[project_id]
return {
"pid": process.pid if process else None,
"running": self.is_running(project_id)
}
return None

228
project_manager.py Normale Datei
Datei anzeigen

@ -0,0 +1,228 @@
"""
Project Manager Module
Handles project storage, loading, and management
"""
import json
import os
from datetime import datetime
from typing import List, Dict, Optional
import uuid
from utils.logger import logger
class Project:
def __init__(self, name: str, path: str, project_id: str = None):
self.id = project_id or str(uuid.uuid4())
self.name = name
self.path = path
self.created_at = datetime.now().isoformat()
self.last_accessed = datetime.now().isoformat()
self.readme_path = os.path.join(path, "CLAUDE_PROJECT_README.md")
self.description = ""
self.tags = []
self.gitea_repo = None # Format: "owner/repo_name" or None if not linked
def to_dict(self) -> Dict:
"""Convert project to dictionary for JSON storage"""
return {
'id': self.id,
'name': self.name,
'path': self.path,
'created_at': self.created_at,
'last_accessed': self.last_accessed,
'readme_path': self.readme_path,
'description': self.description,
'tags': self.tags,
'gitea_repo': self.gitea_repo
}
@classmethod
def from_dict(cls, data: Dict) -> 'Project':
"""Create project from dictionary"""
project = cls(data['name'], data['path'], data['id'])
project.created_at = data.get('created_at', datetime.now().isoformat())
project.last_accessed = data.get('last_accessed', datetime.now().isoformat())
project.readme_path = data.get('readme_path', os.path.join(data['path'], "CLAUDE_PROJECT_README.md"))
project.description = data.get('description', '')
project.tags = data.get('tags', [])
project.gitea_repo = data.get('gitea_repo', None)
return project
def update_last_accessed(self):
"""Update last accessed timestamp"""
self.last_accessed = datetime.now().isoformat()
class ProjectManager:
def __init__(self, data_file: str = "data/projects.json"):
self.data_file = data_file
self.projects: Dict[str, Project] = {}
self.vps_project = None
self.admin_panel_project = None
self.activity_server_project = None
self._ensure_data_dir()
self.load_projects()
self._initialize_vps_project()
self._initialize_admin_panel_project()
self._initialize_activity_server_project()
def _ensure_data_dir(self):
"""Ensure data directory exists"""
data_dir = os.path.dirname(self.data_file)
if data_dir and not os.path.exists(data_dir):
os.makedirs(data_dir)
def _initialize_vps_project(self):
"""Initialize the permanent VPS project"""
vps_id = "vps-permanent"
if vps_id not in self.projects:
self.vps_project = Project(
name="VPS Server",
path="claude-dev@91.99.192.14",
project_id=vps_id
)
self.vps_project.description = "Remote VPS Server with Claude"
self.vps_project.tags = ["vps", "remote", "server"]
self.projects[vps_id] = self.vps_project
self.save_projects()
else:
self.vps_project = self.projects[vps_id]
def _initialize_admin_panel_project(self):
"""Initialize the permanent Admin Panel project"""
admin_id = "admin-panel-permanent"
if admin_id not in self.projects:
self.admin_panel_project = Project(
name="Admin Panel",
path="/opt/v2-Docker",
project_id=admin_id
)
self.admin_panel_project.description = "V2 Docker Admin Panel"
self.admin_panel_project.tags = ["admin", "docker", "v2"]
self.projects[admin_id] = self.admin_panel_project
self.save_projects()
else:
self.admin_panel_project = self.projects[admin_id]
def _initialize_activity_server_project(self):
"""Initialize the permanent Activity Server project"""
activity_id = "activity-server-permanent"
if activity_id not in self.projects:
self.activity_server_project = Project(
name="Activity Server",
path="/home/claude-dev/cpm-activity-server",
project_id=activity_id
)
self.activity_server_project.description = "CPM Activity Server"
self.activity_server_project.tags = ["activity", "server", "cpm"]
self.projects[activity_id] = self.activity_server_project
self.save_projects()
else:
self.activity_server_project = self.projects[activity_id]
def load_projects(self):
"""Load projects from JSON file"""
logger.info("Loading projects from file")
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for proj_data in data.get('projects', []):
project = Project.from_dict(proj_data)
self.projects[project.id] = project
except Exception as e:
print(f"Error loading projects: {e}")
self.projects = {}
def save_projects(self):
"""Save projects to JSON file"""
logger.debug("Saving projects to file")
try:
data = {
'projects': [proj.to_dict() for proj in self.projects.values()],
'last_updated': datetime.now().isoformat()
}
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving projects: {e}")
def add_project(self, name: str, path: str) -> Project:
"""Add new project"""
logger.info(f"Adding project: {name} at {path}")
# Check if project with same path already exists
for project in self.projects.values():
if project.path == path and project.id != "vps-permanent":
# Update existing project
project.name = name
project.update_last_accessed()
self.save_projects()
return project
# Create new project
project = Project(name, path)
self.projects[project.id] = project
self.save_projects()
return project
def remove_project(self, project_id: str) -> bool:
"""Remove project by ID"""
logger.info(f"Removing project with ID: {project_id}")
if project_id in self.projects and project_id not in ["vps-permanent", "admin-panel-permanent"]:
del self.projects[project_id]
self.save_projects()
return True
return False
def get_project(self, project_id: str) -> Optional[Project]:
"""Get project by ID"""
return self.projects.get(project_id)
def get_all_projects(self) -> List[Project]:
"""Get all projects sorted alphabetically by name"""
projects = list(self.projects.values())
# Sort alphabetically, but keep VPS projects first
vps = [p for p in projects if p.id == "vps-permanent"]
admin = [p for p in projects if p.id == "admin-panel-permanent"]
activity = [p for p in projects if p.id == "activity-server-permanent"]
others = [p for p in projects if p.id not in ["vps-permanent", "admin-panel-permanent", "activity-server-permanent"]]
others.sort(key=lambda p: p.name.lower()) # Sort alphabetically by name (case-insensitive)
return vps + admin + activity + others
def update_project(self, project_id: str, **kwargs):
"""Update project properties"""
project = self.get_project(project_id)
if project:
for key, value in kwargs.items():
if hasattr(project, key):
setattr(project, key, value)
self.save_projects()
def search_projects(self, query: str) -> List[Project]:
"""Search projects by name, path, or tags"""
query = query.lower()
results = []
for project in self.projects.values():
if (query in project.name.lower() or
query in project.path.lower() or
any(query in tag.lower() for tag in project.tags)):
results.append(project)
return results
# Test the module
if __name__ == "__main__":
# Create manager
manager = ProjectManager("test_projects.json")
# Add test projects
proj1 = manager.add_project("Test Project 1", "C:\\Users\\test\\project1")
proj2 = manager.add_project("Test Project 2", "C:\\Users\\test\\project2")
# List all projects
print("All projects:")
for project in manager.get_all_projects():
print(f"- {project.name} ({project.path})")
# Clean up test file
import os
if os.path.exists("test_projects.json"):
os.remove("test_projects.json")

65
project_process_tracker.py Normale Datei
Datei anzeigen

@ -0,0 +1,65 @@
"""
Project Process Tracker
Tracks running state of projects in memory
"""
import subprocess
class ProjectProcessTracker:
"""Simple in-memory tracker for project states"""
def __init__(self):
self.running_projects = set()
self.project_manager = None # Will be set by MainWindow
def set_project_manager(self, project_manager):
"""Set reference to project manager"""
self.project_manager = project_manager
def set_running(self, project_id: str):
"""Mark a project as running"""
self.running_projects.add(project_id)
def set_stopped(self, project_id: str):
"""Mark a project as stopped"""
self.running_projects.discard(project_id)
def check_window_exists(self, window_title: str) -> bool:
"""Check if a window with the given title exists"""
try:
# Use PowerShell for faster window checking
ps_command = f'powershell -Command "(Get-Process | Where-Object {{$_.MainWindowTitle -eq \'{window_title}\'}}).Count -gt 0"'
# Hide console window on Windows
startupinfo = None
if hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(ps_command, capture_output=True, text=True, shell=True,
timeout=1, startupinfo=startupinfo)
return result.stdout.strip().lower() == 'true'
except Exception as e:
# Fallback to simple check
return False
def is_running(self, project_id: str) -> bool:
"""Check if a project is running by checking window title"""
if project_id == "vps-permanent":
# Check for VPS window
return self.check_window_exists("Claude VPS Server")
else:
# Check for project window by name
if self.project_manager:
project = self.project_manager.get_project(project_id)
if project:
window_title = f"Claude - {project.name}"
return self.check_window_exists(window_title)
return False
def stop_all(self):
"""Clear all running states"""
self.running_projects.clear()
def get_running_projects(self) -> set:
"""Get all running project IDs"""
return self.running_projects.copy()

295
readme_generator.py Normale Datei
Datei anzeigen

@ -0,0 +1,295 @@
"""
README Generator Module
Automatically generates and updates README files for projects
"""
import os
import json
from datetime import datetime
from typing import Dict, List, Set
import mimetypes
class ReadmeGenerator:
def __init__(self):
self.language_extensions = {
'.py': 'Python',
'.js': 'JavaScript',
'.ts': 'TypeScript',
'.jsx': 'React',
'.tsx': 'React TypeScript',
'.java': 'Java',
'.cpp': 'C++',
'.c': 'C',
'.cs': 'C#',
'.php': 'PHP',
'.rb': 'Ruby',
'.go': 'Go',
'.rs': 'Rust',
'.swift': 'Swift',
'.kt': 'Kotlin',
'.dart': 'Dart',
'.r': 'R',
'.scala': 'Scala',
'.lua': 'Lua',
'.pl': 'Perl',
'.sh': 'Shell',
'.bat': 'Batch',
'.ps1': 'PowerShell',
}
self.framework_indicators = {
'React': ['package.json', 'react'],
'Angular': ['angular.json', '@angular'],
'Vue': ['vue.config.js', 'vue'],
'Django': ['manage.py', 'django'],
'Flask': ['app.py', 'flask'],
'Express': ['express', 'node_modules'],
'Spring': ['pom.xml', 'spring'],
'.NET': ['.csproj', 'dotnet'],
'Laravel': ['artisan', 'laravel'],
'Rails': ['Gemfile', 'rails'],
}
def analyze_project(self, project_path: str) -> Dict:
"""Analyze project structure and content"""
analysis = {
'path': project_path,
'name': os.path.basename(project_path),
'languages': set(),
'frameworks': set(),
'file_count': 0,
'total_size': 0,
'structure': {},
'key_files': [],
'last_modified': None,
}
# Walk through directory
for root, dirs, files in os.walk(project_path):
# Skip hidden and system directories
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['node_modules', '__pycache__', 'venv', 'env']]
rel_path = os.path.relpath(root, project_path)
level = rel_path.count(os.sep)
# Limit depth for structure
if level < 3:
current_dict = analysis['structure']
if rel_path != '.':
for part in rel_path.split(os.sep):
if part not in current_dict:
current_dict[part] = {}
current_dict = current_dict[part]
# Add files to structure
for file in files[:10]: # Limit files shown
if not file.startswith('.'):
current_dict[file] = 'file'
# Analyze files
for file in files:
if file.startswith('.'):
continue
file_path = os.path.join(root, file)
analysis['file_count'] += 1
try:
# Get file size
size = os.path.getsize(file_path)
analysis['total_size'] += size
# Get modification time
mtime = os.path.getmtime(file_path)
if not analysis['last_modified'] or mtime > analysis['last_modified']:
analysis['last_modified'] = mtime
# Detect language
ext = os.path.splitext(file)[1].lower()
if ext in self.language_extensions:
analysis['languages'].add(self.language_extensions[ext])
# Key files
if file in ['README.md', 'package.json', 'requirements.txt', 'Dockerfile',
'.gitignore', 'Makefile', 'setup.py', 'pom.xml']:
analysis['key_files'].append(file)
except:
pass
# Detect frameworks
for framework, indicators in self.framework_indicators.items():
for indicator in indicators:
if any(indicator in str(f) for f in analysis['key_files']):
analysis['frameworks'].add(framework)
# Convert sets to lists for JSON serialization
analysis['languages'] = list(analysis['languages'])
analysis['frameworks'] = list(analysis['frameworks'])
return analysis
def generate_readme(self, project_path: str, project_name: str = None) -> str:
"""Generate README content for a project"""
analysis = self.analyze_project(project_path)
if project_name:
analysis['name'] = project_name
# Format file size
size_mb = analysis['total_size'] / (1024 * 1024)
size_str = f"{size_mb:.1f} MB" if size_mb > 1 else f"{analysis['total_size'] / 1024:.1f} KB"
# Format last modified
if analysis['last_modified']:
last_mod = datetime.fromtimestamp(analysis['last_modified']).strftime('%Y-%m-%d %H:%M')
else:
last_mod = "Unknown"
# Build README content
content = f"""# {analysis['name']}
*This README was automatically generated by Claude Project Manager*
## Project Overview
- **Path**: `{analysis['path']}`
- **Files**: {analysis['file_count']} files
- **Size**: {size_str}
- **Last Modified**: {last_mod}
## Technology Stack
"""
if analysis['languages']:
content += "### Languages\n"
for lang in sorted(analysis['languages']):
content += f"- {lang}\n"
content += "\n"
if analysis['frameworks']:
content += "### Frameworks & Libraries\n"
for framework in sorted(analysis['frameworks']):
content += f"- {framework}\n"
content += "\n"
# Add structure
content += "## Project Structure\n\n```\n"
content += self._format_structure(analysis['structure'])
content += "```\n\n"
# Add key files section
if analysis['key_files']:
content += "## Key Files\n\n"
for file in analysis['key_files']:
content += f"- `{file}`\n"
content += "\n"
# Add Claude usage section
content += """## Claude Integration
This project is managed with Claude Project Manager. To work with this project:
1. Open Claude Project Manager
2. Click on this project's tile
3. Claude will open in the project directory
## Notes
*Add your project-specific notes here*
---
## Development Log
"""
# Add timestamp
content += f"- README generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
return content
def _format_structure(self, structure: Dict, prefix: str = "", is_last: bool = True) -> str:
"""Format directory structure for display"""
output = ""
items = list(structure.items())
for i, (name, value) in enumerate(items):
is_last_item = i == len(items) - 1
# Add tree characters
if prefix:
output += prefix
output += "└── " if is_last_item else "├── "
output += name
if isinstance(value, dict) and value: # It's a directory with contents
output += "/\n"
extension = " " if is_last_item else ""
output += self._format_structure(value, prefix + extension, is_last_item)
else:
output += "\n"
return output
def update_readme(self, readme_path: str, new_content: str):
"""Update README file, preserving user notes"""
if os.path.exists(readme_path):
try:
with open(readme_path, 'r', encoding='utf-8') as f:
old_content = f.read()
# Try to preserve user notes section
notes_marker = "## Notes"
if notes_marker in old_content:
notes_start = old_content.find(notes_marker)
notes_end = old_content.find("\n---", notes_start)
if notes_end > notes_start:
user_notes = old_content[notes_start:notes_end]
# Replace empty notes section with user's notes
new_content = new_content.replace(
"## Notes\n\n*Add your project-specific notes here*\n",
user_notes
)
# Append to development log
if "## Development Log" in old_content:
log_start = old_content.find("## Development Log")
old_log = old_content[log_start:]
# Remove the new log header and just append entries
new_log_entry = f"- README updated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
new_content = new_content[:new_content.find("## Development Log")] + old_log + new_log_entry
except:
pass
# Write the README
with open(readme_path, 'w', encoding='utf-8') as f:
f.write(new_content)
def generate_and_save_readme(self, project_path: str, project_name: str = None) -> str:
"""Generate and save README for a project"""
readme_path = os.path.join(project_path, "CLAUDE_PROJECT_README.md")
content = self.generate_readme(project_path, project_name)
self.update_readme(readme_path, content)
return readme_path
# Test the module
if __name__ == "__main__":
generator = ReadmeGenerator()
# Test with current directory
test_path = os.path.dirname(os.path.abspath(__file__))
analysis = generator.analyze_project(test_path)
print("Project Analysis:")
print(f"Languages: {analysis['languages']}")
print(f"Files: {analysis['file_count']}")
print(f"Key files: {analysis['key_files']}")
# Generate README
readme = generator.generate_readme(test_path, "Claude Project Manager")
print("\nGenerated README Preview:")
print(readme[:500] + "...")

5
requirements.txt Normale Datei
Datei anzeigen

@ -0,0 +1,5 @@
customtkinter>=5.2.0
pillow>=10.0.0
requests>=2.31.0
python-socketio[client]>=5.10.0
websocket-client>=1.6.0

30
scripts/check_lfs_status.bat Normale Datei
Datei anzeigen

@ -0,0 +1,30 @@
@echo off
echo Checking Git LFS Status...
echo =============================
echo.
echo 1. LFS Version:
git lfs version
echo.
echo 2. LFS Environment:
git lfs env
echo.
echo 3. Tracked LFS patterns:
git lfs track
echo.
echo 4. LFS files in repository:
git lfs ls-files
echo.
echo 5. Verifying LFS remote:
git lfs fsck
echo.
echo 6. Current remotes:
git remote -v
echo.
pause

19
scripts/fix_large_files.bat Normale Datei
Datei anzeigen

@ -0,0 +1,19 @@
@echo off
echo Finding large files in repository...
echo.
REM Find files larger than 100MB
echo Files larger than 100MB:
git ls-files -z | xargs -0 -n1 -I{} -- du -h {} | grep -E "^[0-9]+(M|G)" | sort -hr
echo.
echo To add large files to Git LFS:
echo 1. git lfs track "*.zip"
echo 2. git lfs track "*.mp4"
echo 3. git lfs track "path/to/largefile"
echo.
echo Then: git add .gitattributes
echo git add [large files]
echo git commit -m "Add LFS tracking"
echo.
pause

Datei anzeigen

@ -0,0 +1,24 @@
@echo off
echo Fixing Git Remote for IntelSight Organization
echo =============================================
echo.
echo Current remote:
git remote -v
echo.
echo Removing old remote...
git remote remove origin
echo Adding new remote for IntelSight organization...
git remote add origin https://gitea-undso.intelsight.de/IntelSight/website-main.git
echo.
echo New remote:
git remote -v
echo.
echo Now push to the new remote:
echo git push -u origin master
echo.
pause

31
scripts/fix_website_auth.bat Normale Datei
Datei anzeigen

@ -0,0 +1,31 @@
@echo off
echo Fixing Authentication for website-main
echo =====================================
echo.
cd C:\Users\hendr\Desktop\IntelSight\website-main
echo Current remote:
git remote -v
echo.
echo Removing old remote...
git remote remove origin
echo Adding authenticated remote...
echo NOTE: Replace USERNAME and TOKEN with your actual Gitea credentials!
echo.
echo Example:
echo git remote add origin https://USERNAME:TOKEN@gitea-undso.intelsight.de/IntelSight/website-main.git
echo.
echo Your current credentials from ClaudeProjectManager:
echo Username: StuXn3t
echo Token: 327caf5e04a43c35e67e0fb00e0a9cd269889db9
echo.
echo Run this command:
echo git remote add origin https://StuXn3t:327caf5e04a43c35e67e0fb00e0a9cd269889db9@gitea-undso.intelsight.de/IntelSight/website-main.git
echo.
echo Then:
echo git push -u origin master
echo.
pause

Datei anzeigen

@ -0,0 +1,54 @@
@echo off
echo Final fix for website-main...
echo.
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
echo Step 1: Removing Git LFS completely...
git lfs uninstall
echo.
echo Step 2: Checking current status...
git status
echo.
echo Step 3: The video file is missing! Let's check...
if not exist "assets\videos\background.mp4" (
echo ERROR: background.mp4 was removed by LFS!
echo You need to restore it from backup or remove it from the repository.
echo.
echo Option A: Remove from repository
echo Run: git rm assets/videos/background.mp4
echo git commit -m "Remove video file"
echo.
echo Option B: Add to .gitignore
echo Add this line to .gitignore: assets/videos/background.mp4
echo.
) else (
echo File exists. Adding it back...
git add assets/videos/background.mp4
git commit -m "Re-add video file without LFS"
)
echo.
echo Step 4: Removing LFS hooks...
if exist .git\hooks\pre-push (
del .git\hooks\pre-push
echo Removed pre-push hook
)
echo.
echo Step 5: Try push with --no-verify...
git push --no-verify -u origin main
echo.
echo If still failing, the video file might be too large (>100MB).
echo In that case, add it to .gitignore:
echo.
echo echo assets/videos/background.mp4 >> .gitignore
echo git rm --cached assets/videos/background.mp4
echo git add .gitignore
echo git commit -m "Remove large video file"
echo git push -u origin main
echo.
pause

Datei anzeigen

@ -0,0 +1,46 @@
@echo off
echo Fixing website-main LFS and branch issues...
echo.
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
echo Step 1: Checking for LFS files...
git lfs ls-files
echo.
echo Step 2: Removing LFS tracking (if any)...
if exist .gitattributes (
echo Backing up .gitattributes...
copy .gitattributes .gitattributes.backup
echo Removing LFS entries...
powershell -Command "(Get-Content .gitattributes) | Where-Object {$_ -notmatch 'filter=lfs'} | Set-Content .gitattributes.temp"
move /y .gitattributes.temp .gitattributes
git add .gitattributes
git commit -m "Remove LFS tracking" 2>nul
)
echo.
echo Step 3: Checking which files are tracked by LFS...
git lfs ls-files
for /f "tokens=3" %%i in ('git lfs ls-files') do (
echo Removing LFS tracking for: %%i
git rm --cached %%i
git add %%i
)
echo.
echo Step 4: Renaming branch from master to main...
git branch -m master main
echo.
echo Step 5: Pushing to main branch...
git push -u origin main --force
echo.
echo Done! If push still fails, try:
echo 1. Delete .git/hooks/pre-push (if exists)
echo 2. Run: git push --no-verify -u origin main
echo.
pause

37
scripts/fix_website_repo.bat Normale Datei
Datei anzeigen

@ -0,0 +1,37 @@
@echo off
echo Fixing website-main repository...
echo.
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
echo Step 1: Removing old remote...
git remote remove origin 2>nul
echo Step 2: Checking/removing LFS configuration...
if exist .gitattributes (
findstr /C:"filter=lfs" .gitattributes >nul
if %errorlevel%==0 (
echo Found LFS configuration, creating backup...
copy .gitattributes .gitattributes.backup
echo Removing LFS entries...
powershell -Command "(Get-Content .gitattributes) | Where-Object {$_ -notmatch 'filter=lfs'} | Set-Content .gitattributes"
echo Unstaging LFS files...
git rm --cached .gitattributes 2>nul
git add .gitattributes
)
)
echo Step 3: Adding correct remote...
git remote add origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/website-main.git
echo Step 4: Verifying remote...
git remote -v
echo.
echo Ready! Now try:
echo 1. Create the repository on Gitea first (in IntelSight organization)
echo 2. Then run: git push -u origin master:main
echo.
pause

Datei anzeigen

@ -0,0 +1,21 @@
@echo off
echo Quick fix for website-main repository...
echo.
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
echo Current remote:
git remote -v
echo.
echo Fixing remote URL with correct token...
git remote set-url origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/website-main.git
echo.
echo New remote:
git remote -v
echo.
echo Done! Now you can push normally.
echo.
pause

Datei anzeigen

@ -0,0 +1,30 @@
# Large files
*.zip
*.rar
*.7z
*.tar
*.gz
# Media files over 100MB
*.mp4
*.avi
*.mov
*.mkv
*.wmv
# Database dumps
*.sql
*.dump
# Backup files
*.bak
backup/
backups/
# Build artifacts
dist/
build/
node_modules/
# Add specific large files here
# largefile.ext

1
services/__init__.py Normale Datei
Datei anzeigen

@ -0,0 +1 @@
# Services package

326
services/activity_sync.py Normale Datei
Datei anzeigen

@ -0,0 +1,326 @@
"""
Activity Sync Service for CPM
Handles WebSocket connection to the Activity Server
"""
import socketio
import requests
import threading
import json
from typing import Optional, Callable, List, Dict
from pathlib import Path
from utils.logger import logger
class ActivitySyncService:
def __init__(self, server_url: str = None, api_key: str = None, user_id: str = None, user_name: str = None):
self.server_url = server_url or "http://localhost:3001"
self.api_key = api_key
self.user_id = user_id
self.user_name = user_name
self.sio = None
self.connected = False
self.on_activities_update = None
self.activities = []
self.active_projects = [] # Changed from single current_activity to list
# Load settings
self.load_settings()
def load_settings(self):
"""Load activity sync settings from file"""
settings_file = Path.home() / ".claude_project_manager" / "activity_settings.json"
try:
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
self.server_url = settings.get("server_url", self.server_url)
self.api_key = settings.get("api_key", self.api_key)
self.user_id = settings.get("user_id", self.user_id)
self.user_name = settings.get("user_name", self.user_name)
logger.info(f"Loaded activity sync settings from {settings_file}")
except Exception as e:
logger.error(f"Failed to load activity settings: {e}")
def save_settings(self):
"""Save activity sync settings to file"""
settings_file = Path.home() / ".claude_project_manager" / "activity_settings.json"
try:
settings_file.parent.mkdir(parents=True, exist_ok=True)
settings = {
"server_url": self.server_url,
"api_key": self.api_key,
"user_id": self.user_id,
"user_name": self.user_name
}
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
logger.info(f"Saved activity sync settings to {settings_file}")
except Exception as e:
logger.error(f"Failed to save activity settings: {e}")
def connect(self):
"""Connect to the activity server"""
if not all([self.server_url, self.api_key, self.user_id, self.user_name]):
logger.warning("Activity sync not configured properly")
return False
try:
# Create Socket.IO client
self.sio = socketio.Client()
# Register event handlers
@self.sio.event
def connect():
logger.info("Connected to activity server")
self.connected = True
# Fetch initial activities after connection
self._fetch_initial_activities()
@self.sio.event
def disconnect():
logger.info("Disconnected from activity server")
self.connected = False
@self.sio.event
def activities_update(data):
"""Handle activities update from server"""
logger.debug(f"Received raw activities update: {len(data)} items")
# Filter out inactive entries and ensure we only keep active ones
active_activities = [
activity for activity in data
if activity.get('isActive', False)
]
# Log details about the activities
for activity in active_activities:
logger.debug(f"Active: {activity.get('userName')} on {activity.get('projectName')}")
self.activities = active_activities
logger.info(f"Activities update: {len(active_activities)} active (of {len(data)} total)")
if self.on_activities_update:
self.on_activities_update(active_activities)
@self.sio.event
def connect_error(data):
logger.error(f"Connection error: {data}")
self.connected = False
# Connect with authentication
logger.info(f"Attempting to connect to activity server: {self.server_url}")
# Try to force polling transport if WebSockets are blocked
import os
os.environ['SOCKETIO_TRANSPORT'] = 'polling'
self.sio.connect(
self.server_url,
auth={
'token': self.api_key,
'userId': self.user_id,
'userName': self.user_name
},
wait_timeout=10,
transports=['polling', 'websocket'] # Try polling first, then websocket
)
return True
except Exception as e:
logger.error(f"Failed to connect to activity server: {e}")
self.connected = False
return False
def disconnect(self):
"""Disconnect from the activity server"""
if self.sio:
try:
self.sio.disconnect()
self.sio = None
self.connected = False
logger.info("Disconnected from activity server")
except Exception as e:
logger.error(f"Error disconnecting: {e}")
def start_activity(self, project_name: str, project_path: str, description: str = ""):
"""Start a new activity (adds to active projects list)"""
logger.debug(f"start_activity called for: {project_name}")
if not self.connected or not self.sio:
logger.warning("Not connected to activity server")
return False
try:
# Check if project is already active
existing = next((p for p in self.active_projects if p['projectName'] == project_name), None)
if existing:
logger.info(f"Project {project_name} is already active")
return True
# Add to active projects list
new_activity = {
'projectName': project_name,
'projectPath': project_path,
'userId': self.user_id,
'userName': self.user_name,
'isActive': True
}
self.active_projects.append(new_activity)
logger.debug(f"Added to active_projects: {new_activity}")
# Emit to server
self.sio.emit('activity-start', {
'projectName': project_name,
'projectPath': project_path,
'description': description
})
logger.info(f"Started activity for project: {project_name}")
return True
except Exception as e:
logger.error(f"Failed to start activity: {e}")
# Remove from list on failure
self.active_projects = [p for p in self.active_projects if p['projectName'] != project_name]
return False
def stop_activity(self, project_name: str = None):
"""Stop activity for a specific project or all activities if no project specified"""
logger.debug(f"stop_activity called for project: {project_name}")
if not self.connected or not self.sio:
logger.warning("Not connected to activity server")
return False
try:
if project_name:
# Stop specific project
activity = next((p for p in self.active_projects if p['projectName'] == project_name), None)
if not activity:
logger.warning(f"Project {project_name} is not in active projects")
return False
# Remove from active projects
self.active_projects = [p for p in self.active_projects if p['projectName'] != project_name]
logger.debug(f"Removed {project_name} from active_projects")
# NEW: Emit with activityId if available
emit_data = {}
if 'id' in activity:
emit_data['activityId'] = activity['id']
elif 'activityId' in activity:
emit_data['activityId'] = activity['activityId']
else:
# Fallback to project name
emit_data['projectName'] = project_name
self.sio.emit('activity-stop', emit_data)
logger.info(f"Stopped activity for project: {project_name}")
else:
# Stop all activities (backward compatibility)
project_names = [p['projectName'] for p in self.active_projects]
self.active_projects = []
logger.debug("Cleared all active_projects")
# Emit to server (server should handle stopping all activities for this user)
self.sio.emit('activity-stop')
logger.info(f"Stopped all activities: {project_names}")
return True
except Exception as e:
logger.error(f"Failed to stop activity: {e}")
return False
def get_activities(self) -> List[Dict]:
"""Get current activities via REST API"""
if not self.api_key:
return []
try:
response = requests.get(
f"{self.server_url}/api/activities",
headers={"X-API-Key": self.api_key},
timeout=5
)
if response.status_code == 200:
data = response.json()
all_activities = data.get('activities', [])
# Filter to only return active activities
return [a for a in all_activities if a.get('isActive', False)]
else:
logger.error(f"Failed to fetch activities: {response.status_code}")
return []
except Exception as e:
logger.error(f"Failed to fetch activities: {e}")
return []
def is_configured(self) -> bool:
"""Check if the service is properly configured"""
return all([self.server_url, self.api_key, self.user_id, self.user_name])
def get_active_users_count(self) -> int:
"""Get count of active users"""
return len(self.activities)
def is_project_active(self, project_name: str) -> Optional[Dict]:
"""Check if a project has active users"""
for activity in self.activities:
if activity.get('projectName') == project_name and activity.get('isActive'):
return activity
return None
def get_current_activity(self, project_name: str = None) -> Optional[Dict]:
"""Get current user's activity for a specific project or first active project"""
if project_name:
# Return specific project if active
activity = next((p for p in self.active_projects if p['projectName'] == project_name), None)
logger.debug(f"get_current_activity for {project_name}, returning: {activity}")
return activity
else:
# Return first active project for backward compatibility
activity = self.active_projects[0] if self.active_projects else None
logger.debug(f"get_current_activity called, returning: {activity}")
return activity
def get_all_current_activities(self) -> List[Dict]:
"""Get all current user's active projects"""
logger.debug(f"get_all_current_activities called, returning {len(self.active_projects)} activities")
return self.active_projects.copy()
def is_project_active_for_user(self, project_name: str) -> bool:
"""Check if a specific project is active for the current user"""
return any(p['projectName'] == project_name for p in self.active_projects)
def _fetch_initial_activities(self):
"""Fetch initial activities after connection"""
try:
# Fetch user-specific activities
response = requests.get(
f"{self.server_url}/api/activities/{self.user_id}",
headers={"X-API-Key": self.api_key},
timeout=5
)
if response.status_code == 200:
data = response.json()
# NEW: Handle array format
user_activities = data.get('activities', [])
# Update our active projects list
self.active_projects = [a for a in user_activities if a.get('isActive', False)]
logger.info(f"Fetched {len(self.active_projects)} active projects for current user")
# Also fetch all activities
activities = self.get_activities() # Already filtered to only active
if activities:
self.activities = activities
if self.on_activities_update:
self.on_activities_update(activities)
logger.info(f"Fetched {len(activities)} active activities from all users")
except Exception as e:
logger.error(f"Failed to fetch initial activities: {e}")
# Global instance
activity_service = ActivitySyncService()

Datei anzeigen

@ -0,0 +1,72 @@
#!/bin/bash
# Setup script to configure automatic directory change for Activity Server SSH connections
echo "Activity Server Profile Setup Script"
echo "===================================="
echo ""
echo "This script will configure the server to automatically navigate to"
echo "/home/claude-dev/cpm-activity-server when connecting via SSH."
echo ""
# SSH connection details
SSH_HOST="91.99.192.14"
SSH_USER="claude-dev"
SSH_PASS="z0E1Al}q2H?Yqd!O"
# Create the profile modification script
cat << 'PROFILE_SCRIPT' > /tmp/setup_profile.sh
#!/bin/bash
# Add to .bashrc to detect SSH connections and change directory
echo "" >> ~/.bashrc
echo "# Auto-navigate to Activity Server directory for SSH connections" >> ~/.bashrc
echo "if [ -n \"\$SSH_CLIENT\" ] || [ -n \"\$SSH_TTY\" ]; then" >> ~/.bashrc
echo " # Check if we're in an SSH session" >> ~/.bashrc
echo " if [ -d \"/home/claude-dev/cpm-activity-server\" ]; then" >> ~/.bashrc
echo " cd /home/claude-dev/cpm-activity-server" >> ~/.bashrc
echo " echo \"Automatically changed to Activity Server directory\"" >> ~/.bashrc
echo " echo \"\"" >> ~/.bashrc
echo " fi" >> ~/.bashrc
echo "fi" >> ~/.bashrc
echo "Profile updated successfully!"
PROFILE_SCRIPT
# Make the script executable
chmod +x /tmp/setup_profile.sh
echo "Connecting to server to apply configuration..."
echo ""
# Try different methods to connect and run the script
if command -v sshpass >/dev/null 2>&1; then
echo "Using sshpass..."
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST 'bash -s' < /tmp/setup_profile.sh
elif command -v expect >/dev/null 2>&1; then
echo "Using expect..."
expect << EOF
spawn ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST
expect "password:"
send "$SSH_PASS\r"
expect "$ "
send "bash < /tmp/setup_profile.sh\r"
expect "$ "
send "exit\r"
expect eof
EOF
else
echo "Neither sshpass nor expect found."
echo ""
echo "Please manually run the following on the server:"
echo "1. SSH to $SSH_USER@$SSH_HOST"
echo "2. Run the commands in /tmp/setup_profile.sh"
echo ""
echo "Or install sshpass: sudo apt-get install sshpass"
fi
# Clean up
rm -f /tmp/setup_profile.sh
echo ""
echo "Setup complete! Future SSH connections will automatically"
echo "navigate to the Activity Server directory."

14
src/gitea/__init__.py Normale Datei
Datei anzeigen

@ -0,0 +1,14 @@
from .gitea_client import GiteaClient, GiteaConfig
from .git_operations import GitOperationsManager
from .repository_manager import RepositoryManager
from .issue_pr_manager import IssueManager, PullRequestManager, IssuePRManager
__all__ = [
'GiteaClient',
'GiteaConfig',
'GitOperationsManager',
'RepositoryManager',
'IssueManager',
'PullRequestManager',
'IssuePRManager'
]

667
src/gitea/git_operations.py Normale Datei
Datei anzeigen

@ -0,0 +1,667 @@
import os
import subprocess
import logging
from typing import Optional, List, Tuple
from pathlib import Path
from dataclasses import dataclass
import tkinter as tk
from tkinter import filedialog
from datetime import datetime
from utils.logger import logger
import json
@dataclass
class GitCredentials:
username: str
token: str
def get_remote_url(self, base_url: str, repo_path: str) -> str:
from urllib.parse import urlparse
parsed = urlparse(base_url)
url = f"https://{self.username}:{self.token}@{parsed.netloc}/{repo_path}.git"
logger.info(f"Generated remote URL: https://{self.username}:****@{parsed.netloc}/{repo_path}.git")
return url
class GitOperationsManager:
def __init__(self, base_url: str, token: str, username: str = "claude-project-manager"):
self.base_url = base_url
self.credentials = GitCredentials(username=username, token=token)
self.default_clone_dir = self._get_clone_directory()
def _get_clone_directory(self) -> Path:
"""Get clone directory from settings or use default"""
try:
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
clone_dir = settings.get("gitea_clone_directory")
if clone_dir:
logger.info(f"Loaded clone directory from settings: {clone_dir}")
return Path(clone_dir)
else:
logger.info("No gitea_clone_directory in settings")
else:
logger.info("Settings file does not exist")
except Exception as e:
logger.warning(f"Could not load clone directory from settings: {e}")
# Return default if not found in settings
default_dir = Path.home() / "GiteaRepos"
logger.info(f"Using default clone directory: {default_dir}")
return default_dir
def select_directory(self, title: str = "Select Clone Directory") -> Optional[Path]:
root = tk.Tk()
root.withdraw()
root.attributes('-topmost', True)
directory = filedialog.askdirectory(
title=title,
initialdir=str(self.default_clone_dir),
mustexist=False
)
root.destroy()
if directory:
path = Path(directory)
path.mkdir(parents=True, exist_ok=True)
return path
return None
def _run_git_command(self, cmd: List[str], cwd: Optional[Path] = None) -> Tuple[bool, str, str]:
try:
# Log the command being executed
cmd_str = ' '.join(cmd)
logger.info(f"Executing git command: {cmd_str}")
if cwd:
logger.debug(f"Working directory: {cwd}")
# 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(
cmd,
cwd=cwd,
capture_output=True,
text=True,
timeout=60,
startupinfo=startupinfo
)
success = result.returncode == 0
stdout = result.stdout.strip()
stderr = result.stderr.strip()
if success:
logger.debug(f"Git command successful: {cmd_str}")
if stdout:
logger.debug(f"Output: {stdout[:500]}...") # Log first 500 chars
else:
logger.error(f"Git command failed: {cmd_str}")
logger.error(f"Error: {stderr}")
return success, stdout, stderr
except subprocess.TimeoutExpired:
logger.error(f"Git command timed out: {' '.join(cmd)}")
return False, "", "Command timed out"
except Exception as e:
logger.error(f"Git command error: {e}")
return False, "", str(e)
def clone_repository(self, owner: str, repo: str, clone_dir: Optional[Path] = None) -> Tuple[bool, Path]:
if clone_dir is None:
# Use default clone directory if none specified
clone_dir = self.default_clone_dir
logger.info(f"Using default clone directory: {clone_dir}")
clone_dir.mkdir(parents=True, exist_ok=True)
else:
logger.info(f"Using provided clone directory: {clone_dir}")
repo_path = f"{owner}/{repo}"
remote_url = self.credentials.get_remote_url(self.base_url, repo_path)
local_path = clone_dir / repo
if local_path.exists():
logger.warning(f"Repository already exists at {local_path}")
return True, local_path # Return success if already exists
cmd = ["git", "clone", remote_url, str(local_path)]
success, stdout, stderr = self._run_git_command(cmd)
if success:
logger.info(f"Successfully cloned {repo} to {local_path}")
self._configure_git_user(local_path)
else:
logger.error(f"Clone failed: {stderr}")
return success, local_path
def _configure_git_user(self, repo_path: Path) -> None:
self._run_git_command(["git", "config", "user.name", "Claude Project Manager"], cwd=repo_path)
self._run_git_command(["git", "config", "user.email", "claude@intelsight.de"], cwd=repo_path)
def fetch(self, repo_path: Path, remote: str = "origin") -> Tuple[bool, str]:
cmd = ["git", "fetch", remote]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def pull(self, repo_path: Path, remote: str = "origin", branch: Optional[str] = None) -> Tuple[bool, str]:
cmd = ["git", "pull", remote]
if branch:
cmd.append(branch)
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def add(self, repo_path: Path, files: List[str] = None) -> Tuple[bool, str]:
if files is None:
files = ["."]
cmd = ["git", "add"] + files
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def commit(self, repo_path: Path, message: str) -> Tuple[bool, str]:
cmd = ["git", "commit", "-m", message]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
if not success and "nothing to commit" in stderr:
return True, "Nothing to commit, working tree clean"
return success, stdout or stderr
def split_large_file(self, file_path: Path, chunk_size_mb: int = 45) -> List[Path]:
"""Split a large file into smaller chunks"""
chunk_size = chunk_size_mb * 1024 * 1024 # Convert to bytes
chunks = []
try:
file_size = file_path.stat().st_size
with open(file_path, 'rb') as f:
chunk_num = 0
while True:
chunk_data = f.read(chunk_size)
if not chunk_data:
break
chunk_path = file_path.parent / f"{file_path.stem}.part{chunk_num:03d}{file_path.suffix}"
with open(chunk_path, 'wb') as chunk_file:
chunk_file.write(chunk_data)
chunks.append(chunk_path)
chunk_num += 1
logger.info(f"Split {file_path.name} into {len(chunks)} chunks")
return chunks
except Exception as e:
logger.error(f"Error splitting file: {e}")
return []
def create_join_script(self, original_file: Path, chunks: List[Path]) -> Path:
"""Create a script to rejoin file chunks"""
script_path = original_file.parent / f"join_{original_file.stem}.bat"
with open(script_path, 'w') as f:
f.write("@echo off\n")
f.write(f"echo Joining {original_file.name}...\n")
f.write(f"copy /b ")
# Add all chunks
for i, chunk in enumerate(chunks):
if i > 0:
f.write("+")
f.write(f'"{chunk.name}"')
f.write(f' "{original_file.name}"\n')
f.write("echo Done!\n")
f.write("pause\n")
# Also create a shell script for Unix
sh_script_path = original_file.parent / f"join_{original_file.stem}.sh"
with open(sh_script_path, 'w') as f:
f.write("#!/bin/bash\n")
f.write(f"echo 'Joining {original_file.name}...'\n")
f.write("cat ")
for chunk in chunks:
f.write(f'"{chunk.name}" ')
f.write(f'> "{original_file.name}"\n')
f.write("echo 'Done!'\n")
return script_path
def check_large_files(self, repo_path: Path, size_limit_mb: int = 100) -> List[Tuple[str, int]]:
"""Check for files larger than size_limit_mb in the repository"""
large_files = []
try:
# Get list of all tracked files
cmd = ["git", "ls-files", "-z"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
if success and stdout:
files = stdout.split('\0')
for file in files:
if file:
file_path = repo_path / file
if file_path.exists():
size = file_path.stat().st_size
if size > size_limit_mb * 1024 * 1024:
large_files.append((file, size))
except Exception as e:
logger.error(f"Error checking large files: {e}")
return large_files
def push(self, repo_path: Path, remote: str = "origin", branch: Optional[str] = None) -> Tuple[bool, str]:
# Get current branch first
branch_cmd = ["git", "branch", "--show-current"]
branch_success, current_branch, _ = self._run_git_command(branch_cmd, cwd=repo_path)
current_branch = current_branch.strip() if branch_success else ""
# First try regular push
cmd = ["git", "push", remote]
if branch:
cmd.append(branch)
elif current_branch == "master":
# If we're on master and no branch specified, push to main
cmd.extend(["HEAD:main"])
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
# If push failed due to no upstream or branch name mismatch, try with --set-upstream
if not success and ("has no upstream branch" in stderr or "The current branch" in stderr or
"--set-upstream" in stderr or "does not match" in stderr):
if current_branch:
# Retry with --set-upstream
# If we're on master, push to main (common convention)
remote_branch = "main" if current_branch == "master" else current_branch
cmd = ["git", "push", "--set-upstream", remote, f"{current_branch}:{remote_branch}"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
# Check for specific errors
if not success:
if "HTTP 413" in stderr or "Request Entity Too Large" in stderr:
# Extract file info from error if possible
import re
file_match = re.search(r'/([^/]+)/(\d+)$', stderr)
file_size = None
if file_match:
file_hash = file_match.group(1)
file_size = int(file_match.group(2))
# Check for large files
large_files = self.check_large_files(repo_path, 50) # Check for files > 50MB
if large_files:
error_msg = "Push fehlgeschlagen: Dateien zu groß für Gitea!\n\n"
error_msg += "Folgende Dateien überschreiten das Limit:\n"
for file, size in large_files[:5]: # Show first 5
size_mb = size / (1024 * 1024)
error_msg += f" - {file} ({size_mb:.1f} MB)\n"
if len(large_files) > 5:
error_msg += f" ... und {len(large_files) - 5} weitere\n"
if file_size:
error_msg += f"\nGitea versucht eine {file_size / (1024*1024):.1f} MB große Datei hochzuladen!\n"
error_msg += "\nLösungen:\n"
error_msg += "1. Große Dateien zur .gitignore hinzufügen\n"
error_msg += "2. Git LFS verwenden (empfohlen für große Dateien)\n"
error_msg += "3. Dateien komprimieren oder entfernen\n\n"
error_msg += "HINWEIS: Git LFS muss auf dem Server aktiviert sein!"
return False, error_msg
else:
return False, f"Push fehlgeschlagen (HTTP 413): Repository oder einzelne Commits zu groß"
elif "Locking support detected" in stderr and "HTTP 413" not in stderr:
# Just LFS warning, not an error
return True, "Push erfolgreich (mit LFS-Warnung)"
# Check if push was successful even with warnings
if success or "Everything up-to-date" in (stdout + stderr):
return True, "Push erfolgreich"
return success, stdout or stderr
def status(self, repo_path: Path) -> Tuple[bool, str]:
cmd = ["git", "status", "--porcelain"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def diff(self, repo_path: Path, staged: bool = False) -> Tuple[bool, str]:
cmd = ["git", "diff"]
if staged:
cmd.append("--staged")
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def branch(self, repo_path: Path, list_all: bool = False) -> Tuple[bool, str]:
cmd = ["git", "branch"]
if list_all:
cmd.append("-a")
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def checkout(self, repo_path: Path, branch: str, create: bool = False) -> Tuple[bool, str]:
cmd = ["git", "checkout"]
if create:
cmd.append("-b")
cmd.append(branch)
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def merge(self, repo_path: Path, branch: str) -> Tuple[bool, str]:
cmd = ["git", "merge", branch]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def log(self, repo_path: Path, n: int = 10, oneline: bool = True) -> Tuple[bool, str]:
cmd = ["git", "log", f"-{n}"]
if oneline:
cmd.append("--oneline")
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def remote_add(self, repo_path: Path, name: str, url: str) -> Tuple[bool, str]:
cmd = ["git", "remote", "add", name, url]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def remote_remove(self, repo_path: Path, name: str) -> Tuple[bool, str]:
cmd = ["git", "remote", "remove", name]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def remote_list(self, repo_path: Path) -> Tuple[bool, str]:
cmd = ["git", "remote", "-v"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def remote(self, repo_path: Path, verbose: bool = False) -> Tuple[bool, str]:
"""Alias for remote_list for backward compatibility"""
if verbose:
return self.remote_list(repo_path)
else:
cmd = ["git", "remote"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def stash(self, repo_path: Path, message: Optional[str] = None) -> Tuple[bool, str]:
cmd = ["git", "stash", "push"]
if message:
cmd.extend(["-m", message])
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def stash_pop(self, repo_path: Path) -> Tuple[bool, str]:
cmd = ["git", "stash", "pop"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def reset(self, repo_path: Path, hard: bool = False, commit: Optional[str] = None) -> Tuple[bool, str]:
cmd = ["git", "reset"]
if hard:
cmd.append("--hard")
if commit:
cmd.append(commit)
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return success, stdout or stderr
def init_repository(self, repo_path: Path) -> Tuple[bool, str]:
"""Initialize a new git repository"""
cmd = ["git", "init"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
if success:
self._configure_git_user(repo_path)
# Check if .gitattributes exists with LFS entries
gitattributes = repo_path / ".gitattributes"
if gitattributes.exists():
with open(gitattributes, 'r') as f:
content = f.read()
if 'filter=lfs' in content:
logger.info("Git LFS entries found in .gitattributes")
return success, stdout or stderr
def disable_lfs_for_push(self, repo_path: Path) -> Tuple[bool, str]:
"""Temporarily disable LFS for push"""
# Backup .gitattributes if it exists
gitattributes = repo_path / ".gitattributes"
backup_path = repo_path / ".gitattributes.backup"
if gitattributes.exists():
import shutil
shutil.copy2(gitattributes, backup_path)
# Remove LFS entries from .gitattributes
with open(gitattributes, 'r') as f:
lines = f.readlines()
with open(gitattributes, 'w') as f:
for line in lines:
if 'filter=lfs' not in line:
f.write(line)
return True, "LFS temporarily disabled"
return True, "No LFS configuration found"
def restore_lfs_config(self, repo_path: Path) -> None:
"""Restore LFS configuration"""
backup_path = repo_path / ".gitattributes.backup"
gitattributes = repo_path / ".gitattributes"
if backup_path.exists():
import shutil
shutil.move(backup_path, gitattributes)
def add_remote_to_existing_repo(self, repo_path: Path, owner: str, repo_name: str,
remote_name: str = "origin") -> Tuple[bool, str]:
"""Add Gitea remote to an existing local repository"""
repo_path_str = f"{owner}/{repo_name}"
remote_url = self.credentials.get_remote_url(self.base_url, repo_path_str)
# First check if remote already exists
success, remotes, _ = self._run_git_command(["git", "remote"], cwd=repo_path)
if success and remote_name in remotes.split('\n'):
# Remove existing remote
self.remote_remove(repo_path, remote_name)
# Add new remote
return self.remote_add(repo_path, remote_name, remote_url)
def push_existing_repo_to_gitea(self, repo_path: Path, owner: str, repo_name: str,
branch: str = "main", force: bool = False) -> Tuple[bool, str]:
"""Push an existing local repository to Gitea"""
logger.info(f"=== Starting push_existing_repo_to_gitea ===")
logger.info(f"Repo path: {repo_path}")
logger.info(f"Owner: {owner}")
logger.info(f"Repo name: {repo_name}")
logger.info(f"Target branch: {branch}")
# Create debug info file
debug_file = repo_path / "gitea_push_debug.txt"
debug_info = []
debug_info.append(f"Push Debug Info - {datetime.now()}")
debug_info.append(f"Repository: {repo_name}")
debug_info.append(f"Owner: {owner}")
debug_info.append(f"Path: {repo_path}")
# First check current branch
success, current_branch_output, _ = self._run_git_command(
["git", "branch", "--show-current"], cwd=repo_path
)
current_branch = current_branch_output.strip() if success else ""
debug_info.append(f"Current branch: {current_branch}")
# If no current branch (empty repo), use the specified branch
if not current_branch:
current_branch = branch
# Add the remote
success, msg = self.add_remote_to_existing_repo(repo_path, owner, repo_name)
if not success:
error_msg = f"Failed to add remote: {msg}"
debug_info.append(f"ERROR: {error_msg}")
with open(debug_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(debug_info))
return False, error_msg
# Log the remote URL for debugging
success, remotes, _ = self._run_git_command(["git", "remote", "-v"], cwd=repo_path)
if success:
logger.info(f"Git remotes after adding: {remotes}")
debug_info.append(f"Git remotes:\n{remotes}")
# Check git status before push
success, status, _ = self._run_git_command(["git", "status", "--porcelain"], cwd=repo_path)
debug_info.append(f"Git status before push:\n{status if status else 'Clean'}")
# Push the current branch with upstream setting
cmd = ["git", "push", "--set-upstream", "origin", f"{current_branch}:{branch}", "-v"]
if force:
cmd.insert(2, "--force")
logger.info(f"Executing push command: {' '.join(cmd)}")
debug_info.append(f"Push command: {' '.join(cmd)}")
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
logger.info(f"Push command result - Success: {success}")
logger.info(f"Push stdout: {stdout}")
logger.info(f"Push stderr: {stderr}")
debug_info.append(f"Push result: {'Success' if success else 'Failed'}")
debug_info.append(f"Push stdout:\n{stdout}")
debug_info.append(f"Push stderr:\n{stderr}")
# Save debug info
with open(debug_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(debug_info))
# Check if push was successful even with warnings
if success or "Locking support detected" in stderr:
# Also check if objects were written
if "objects" in stderr or "objects" in stdout or success:
# Verify the push by checking the remote
verify_cmd = ["git", "ls-remote", "origin", branch]
verify_success, verify_out, _ = self._run_git_command(verify_cmd, cwd=repo_path)
if verify_success and verify_out.strip():
logger.info(f"Push verified - remote has branch {branch}")
return True, f"Successfully pushed {current_branch} to {branch}\n\nDebug-Datei: {debug_file}"
else:
logger.warning(f"Push reported success but could not verify remote branch")
return True, f"Push completed (verification pending)\n\nDebug-Datei: {debug_file}"
# Check for common push success indicators
if any(indicator in (stdout + stderr).lower() for indicator in ["everything up-to-date", "branch", "->", "create mode", "writing objects"]):
return True, f"Successfully pushed {current_branch} to {branch}\n\nDebug-Datei: {debug_file}"
return False, f"{stderr or stdout}\n\nDebug-Datei: {debug_file}"
def setup_lfs(self, repo_path: Path) -> Tuple[bool, str]:
"""Setup Git LFS for the repository"""
# First check if git-lfs is installed
cmd = ["git", "lfs", "version"]
success, stdout, stderr = self._run_git_command(cmd)
if not success:
return False, "Git LFS ist nicht installiert. Bitte installieren Sie es von: https://git-lfs.github.com/"
# Install LFS hooks in the repository
cmd = ["git", "lfs", "install"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
if not success:
return False, f"Git LFS Installation fehlgeschlagen: {stderr}"
# Configure LFS for the remote
cmd = ["git", "config", "lfs.https://gitea-undso.intelsight.de/.locksverify", "true"]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
return True, "Git LFS wurde erfolgreich eingerichtet"
def track_with_lfs(self, repo_path: Path, patterns: List[str]) -> Tuple[bool, str]:
"""Track file patterns with Git LFS"""
results = []
for pattern in patterns:
cmd = ["git", "lfs", "track", pattern]
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
if success:
results.append(f"{pattern}")
else:
results.append(f"{pattern}: {stderr}")
# Add .gitattributes to git
cmd = ["git", "add", ".gitattributes"]
self._run_git_command(cmd, cwd=repo_path)
return True, "LFS Tracking konfiguriert:\n" + "\n".join(results)
def migrate_to_lfs(self, repo_path: Path, file_paths: List[str]) -> Tuple[bool, str]:
"""Migrate existing large files to Git LFS"""
# First setup LFS
success, msg = self.setup_lfs(repo_path)
if not success:
return False, msg
# Track the files
patterns = []
for file_path in file_paths:
# Create pattern from file path
if "/" in file_path:
# Track specific file
patterns.append(file_path)
else:
# Track by extension
ext = Path(file_path).suffix
if ext:
patterns.append(f"*{ext}")
if patterns:
success, msg = self.track_with_lfs(repo_path, patterns)
if not success:
return False, msg
# Remove files from git cache and re-add them
for file_path in file_paths:
# Remove from cache
cmd = ["git", "rm", "--cached", file_path]
self._run_git_command(cmd, cwd=repo_path)
# Re-add (will now use LFS)
cmd = ["git", "add", file_path]
self._run_git_command(cmd, cwd=repo_path)
return True, "Dateien wurden zu Git LFS migriert. Bitte committen Sie die Änderungen."
# Create global instance function
def create_git_ops():
"""Create a new GitOperationsManager instance with current config"""
from src.gitea.gitea_client import gitea_config
return GitOperationsManager(
base_url=gitea_config.base_url,
token=gitea_config.api_token,
username=gitea_config.username
)
# Global instance
git_ops = None
def init_git_ops():
"""Initialize the global git_ops instance"""
global git_ops
git_ops = create_git_ops()
# Initialize on import
init_git_ops()

238
src/gitea/gitea_client.py Normale Datei
Datei anzeigen

@ -0,0 +1,238 @@
import requests
import json
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
from datetime import datetime
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class GiteaConfig:
base_url: str = "https://gitea-undso.intelsight.de"
api_token: str = "3b4a6ba1ade3f34640f3c85d2333b4a3a0627471"
api_version: str = "v1"
username: str = "StuXn3t"
def __post_init__(self):
"""Load settings from config file if available"""
self.load_from_settings()
def load_from_settings(self):
"""Load Gitea settings from UI settings file"""
try:
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
# Override with saved settings if available
if "gitea_server_url" in settings and settings["gitea_server_url"]:
self.base_url = settings["gitea_server_url"]
if "gitea_api_token" in settings and settings["gitea_api_token"]:
self.api_token = settings["gitea_api_token"]
if "gitea_username" in settings and settings["gitea_username"]:
self.username = settings["gitea_username"]
logger.info(f"Loaded Gitea settings from config file")
except Exception as e:
logger.warning(f"Could not load Gitea settings from config: {e}")
@property
def api_url(self) -> str:
return f"{self.base_url}/api/{self.api_version}"
class GiteaClient:
def __init__(self, config: Optional[GiteaConfig] = None):
self.config = config or GiteaConfig()
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"token {self.config.api_token}",
"Content-Type": "application/json"
})
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
url = f"{self.config.api_url}/{endpoint}"
try:
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response.json() if response.content else {}
except requests.exceptions.RequestException as e:
logger.error(f"API request failed: {e}")
raise
def get_user_info(self) -> Dict[str, Any]:
return self._request("GET", "user")
def list_user_organizations(self) -> List[Dict[str, Any]]:
"""List all organizations the user is a member of"""
return self._request("GET", "user/orgs")
def get_user_teams_in_org(self, org_name: str) -> List[Dict[str, Any]]:
"""Get user's teams in a specific organization"""
try:
return self._request("GET", f"user/teams", params={"org": org_name})
except Exception as e:
logger.error(f"Failed to get teams for org {org_name}: {e}")
return []
def list_repositories(self, page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
params = {"page": page, "limit": limit}
return self._request("GET", "user/repos", params=params)
def create_repository(self, name: str, description: str = "", private: bool = False,
auto_init: bool = True, gitignores: str = "", license: str = "") -> Dict[str, Any]:
data = {
"name": name,
"description": description,
"private": private,
"auto_init": auto_init,
"gitignores": gitignores,
"license": license,
"default_branch": "main" # Use main instead of master
}
return self._request("POST", "user/repos", json=data)
def delete_repository(self, owner: str, repo: str) -> None:
self._request("DELETE", f"repos/{owner}/{repo}")
def get_repository(self, owner: str, repo: str) -> Dict[str, Any]:
return self._request("GET", f"repos/{owner}/{repo}")
def fork_repository(self, owner: str, repo: str, organization: Optional[str] = None) -> Dict[str, Any]:
data = {"organization": organization} if organization else {}
return self._request("POST", f"repos/{owner}/{repo}/forks", json=data)
def list_issues(self, owner: str, repo: str, state: str = "open",
labels: Optional[List[str]] = None, page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
params = {
"state": state,
"page": page,
"limit": limit
}
if labels:
params["labels"] = ",".join(labels)
return self._request("GET", f"repos/{owner}/{repo}/issues", params=params)
def create_issue(self, owner: str, repo: str, title: str, body: str = "",
assignees: Optional[List[str]] = None, labels: Optional[List[int]] = None) -> Dict[str, Any]:
data = {
"title": title,
"body": body,
"assignees": assignees or [],
"labels": labels or []
}
return self._request("POST", f"repos/{owner}/{repo}/issues", json=data)
def update_issue(self, owner: str, repo: str, index: int, **kwargs) -> Dict[str, Any]:
return self._request("PATCH", f"repos/{owner}/{repo}/issues/{index}", json=kwargs)
def close_issue(self, owner: str, repo: str, index: int) -> Dict[str, Any]:
return self.update_issue(owner, repo, index, state="closed")
def list_pull_requests(self, owner: str, repo: str, state: str = "open",
page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
params = {
"state": state,
"page": page,
"limit": limit
}
return self._request("GET", f"repos/{owner}/{repo}/pulls", params=params)
def create_pull_request(self, owner: str, repo: str, title: str, head: str, base: str,
body: str = "", assignees: Optional[List[str]] = None) -> Dict[str, Any]:
data = {
"title": title,
"head": head,
"base": base,
"body": body,
"assignees": assignees or []
}
return self._request("POST", f"repos/{owner}/{repo}/pulls", json=data)
def merge_pull_request(self, owner: str, repo: str, index: int,
merge_style: str = "merge") -> Dict[str, Any]:
data = {"Do": merge_style}
return self._request("POST", f"repos/{owner}/{repo}/pulls/{index}/merge", json=data)
def list_branches(self, owner: str, repo: str) -> List[Dict[str, Any]]:
return self._request("GET", f"repos/{owner}/{repo}/branches")
def create_branch(self, owner: str, repo: str, branch_name: str,
old_branch_name: str = "main") -> Dict[str, Any]:
data = {
"new_branch_name": branch_name,
"old_branch_name": old_branch_name
}
return self._request("POST", f"repos/{owner}/{repo}/branches", json=data)
def delete_branch(self, owner: str, repo: str, branch_name: str) -> None:
self._request("DELETE", f"repos/{owner}/{repo}/branches/{branch_name}")
def list_releases(self, owner: str, repo: str, page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
params = {"page": page, "limit": limit}
return self._request("GET", f"repos/{owner}/{repo}/releases", params=params)
def create_release(self, owner: str, repo: str, tag_name: str, name: str,
body: str = "", target: str = "main", draft: bool = False,
prerelease: bool = False) -> Dict[str, Any]:
data = {
"tag_name": tag_name,
"name": name,
"body": body,
"target_commitish": target,
"draft": draft,
"prerelease": prerelease
}
return self._request("POST", f"repos/{owner}/{repo}/releases", json=data)
def list_webhooks(self, owner: str, repo: str) -> List[Dict[str, Any]]:
return self._request("GET", f"repos/{owner}/{repo}/hooks")
def create_webhook(self, owner: str, repo: str, url: str, events: List[str],
active: bool = True) -> Dict[str, Any]:
data = {
"type": "gitea",
"config": {
"url": url,
"content_type": "json"
},
"events": events,
"active": active
}
return self._request("POST", f"repos/{owner}/{repo}/hooks", json=data)
def get_repository_contents(self, owner: str, repo: str, filepath: str = "",
ref: str = "main") -> Dict[str, Any]:
params = {"ref": ref}
return self._request("GET", f"repos/{owner}/{repo}/contents/{filepath}", params=params)
def create_or_update_file(self, owner: str, repo: str, filepath: str, content: str,
message: str, branch: str = "main", sha: Optional[str] = None) -> Dict[str, Any]:
import base64
data = {
"content": base64.b64encode(content.encode()).decode(),
"message": message,
"branch": branch
}
if sha:
data["sha"] = sha
return self._request("PUT", f"repos/{owner}/{repo}/contents/{filepath}", json=data)
def delete_file(self, owner: str, repo: str, filepath: str, message: str,
sha: str, branch: str = "main") -> Dict[str, Any]:
data = {
"message": message,
"sha": sha,
"branch": branch
}
return self._request("DELETE", f"repos/{owner}/{repo}/contents/{filepath}", json=data)
# Global config instance
gitea_config = GiteaConfig()
# Default client instance
client = GiteaClient(gitea_config)

509
src/gitea/gitea_ui.py Normale Datei
Datei anzeigen

@ -0,0 +1,509 @@
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import threading
from pathlib import Path
from typing import Optional, Dict, Any, List
from .repository_manager import RepositoryManager
from .issue_pr_manager import IssuePRManager
class GiteaIntegrationUI:
def __init__(self, parent_frame: tk.Frame):
self.parent = parent_frame
self.repo_manager = RepositoryManager()
self.issue_pr_manager = IssuePRManager(self.repo_manager.client)
self.selected_repo = None
self.selected_repo_path = None
self.setup_ui()
self.refresh_repositories()
def setup_ui(self):
main_paned = ttk.PanedWindow(self.parent, orient=tk.HORIZONTAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
left_frame = ttk.Frame(main_paned)
self.setup_left_panel(left_frame)
main_paned.add(left_frame, weight=1)
right_frame = ttk.Frame(main_paned)
self.setup_right_panel(right_frame)
main_paned.add(right_frame, weight=2)
def setup_left_panel(self, parent):
ttk.Label(parent, text="Repositories", font=("Arial", 12, "bold")).pack(pady=5)
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, padx=5)
ttk.Button(button_frame, text="🔄 Refresh", command=self.refresh_repositories).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text=" New", command=self.create_repository_dialog).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="🗑️ Delete", command=self.delete_repository).pack(side=tk.LEFT, padx=2)
list_frame = ttk.Frame(parent)
list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.repo_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set)
self.repo_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.repo_listbox.bind('<<ListboxSelect>>', self.on_repo_select)
scrollbar.config(command=self.repo_listbox.yview)
def setup_right_panel(self, parent):
self.notebook = ttk.Notebook(parent)
self.notebook.pack(fill=tk.BOTH, expand=True)
self.git_tab = ttk.Frame(self.notebook)
self.setup_git_operations_tab(self.git_tab)
self.notebook.add(self.git_tab, text="Git Operations")
self.issues_tab = ttk.Frame(self.notebook)
self.setup_issues_tab(self.issues_tab)
self.notebook.add(self.issues_tab, text="Issues")
self.pr_tab = ttk.Frame(self.notebook)
self.setup_pr_tab(self.pr_tab)
self.notebook.add(self.pr_tab, text="Pull Requests")
self.info_tab = ttk.Frame(self.notebook)
self.setup_info_tab(self.info_tab)
self.notebook.add(self.info_tab, text="Repository Info")
def setup_git_operations_tab(self, parent):
button_frame = ttk.LabelFrame(parent, text="Repository Actions", padding=10)
button_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Button(button_frame, text="📥 Clone", command=self.clone_repository).grid(row=0, column=0, padx=5, pady=2)
ttk.Button(button_frame, text="🔄 Fetch", command=self.fetch_repository).grid(row=0, column=1, padx=5, pady=2)
ttk.Button(button_frame, text="⬇️ Pull", command=self.pull_repository).grid(row=0, column=2, padx=5, pady=2)
ttk.Button(button_frame, text="📊 Status", command=self.show_status).grid(row=0, column=3, padx=5, pady=2)
ttk.Button(button_frame, text=" Add All", command=self.add_all_files).grid(row=1, column=0, padx=5, pady=2)
ttk.Button(button_frame, text="💾 Commit", command=self.commit_dialog).grid(row=1, column=1, padx=5, pady=2)
ttk.Button(button_frame, text="⬆️ Push", command=self.push_repository).grid(row=1, column=2, padx=5, pady=2)
ttk.Button(button_frame, text="🌿 Branches", command=self.show_branches).grid(row=1, column=3, padx=5, pady=2)
output_frame = ttk.LabelFrame(parent, text="Output", padding=10)
output_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.git_output = scrolledtext.ScrolledText(output_frame, height=20, wrap=tk.WORD)
self.git_output.pack(fill=tk.BOTH, expand=True)
def setup_issues_tab(self, parent):
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Button(button_frame, text="🔄 Refresh", command=self.refresh_issues).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text=" New Issue", command=self.create_issue_dialog).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="✏️ Edit", command=self.edit_issue).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="✅ Close", command=self.close_issue).pack(side=tk.LEFT, padx=2)
columns = ('ID', 'Title', 'State', 'Author', 'Created')
self.issues_tree = ttk.Treeview(parent, columns=columns, show='tree headings')
for col in columns:
self.issues_tree.heading(col, text=col)
self.issues_tree.column(col, width=100)
self.issues_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.issues_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.issues_tree.configure(yscrollcommand=scrollbar.set)
def setup_pr_tab(self, parent):
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Button(button_frame, text="🔄 Refresh", command=self.refresh_prs).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text=" New PR", command=self.create_pr_dialog).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="🔀 Merge", command=self.merge_pr).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="❌ Close", command=self.close_pr).pack(side=tk.LEFT, padx=2)
columns = ('ID', 'Title', 'State', 'Author', 'Head', 'Base')
self.pr_tree = ttk.Treeview(parent, columns=columns, show='tree headings')
for col in columns:
self.pr_tree.heading(col, text=col)
self.pr_tree.column(col, width=100)
self.pr_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.pr_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.pr_tree.configure(yscrollcommand=scrollbar.set)
def setup_info_tab(self, parent):
self.info_text = scrolledtext.ScrolledText(parent, height=20, wrap=tk.WORD)
self.info_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
def refresh_repositories(self):
def fetch():
try:
repos = self.repo_manager.list_all_repositories()
self.parent.after(0, lambda: self.update_repo_list(repos))
except Exception as e:
self.parent.after(0, lambda: messagebox.showerror("Error", f"Failed to fetch repositories: {e}"))
threading.Thread(target=fetch, daemon=True).start()
def update_repo_list(self, repos):
self.repo_listbox.delete(0, tk.END)
for repo in repos:
display_name = f"{repo['name']} {'🔒' if repo['private'] else '🌐'}"
self.repo_listbox.insert(tk.END, display_name)
self.repositories = repos
def on_repo_select(self, event):
selection = self.repo_listbox.curselection()
if selection:
index = selection[0]
self.selected_repo = self.repositories[index]
self.update_repo_info()
self.refresh_issues()
self.refresh_prs()
def update_repo_info(self):
if not self.selected_repo:
return
info = f"Repository: {self.selected_repo['name']}\n"
info += f"Description: {self.selected_repo.get('description', 'No description')}\n"
info += f"Private: {'Yes' if self.selected_repo['private'] else 'No'}\n"
info += f"Default Branch: {self.selected_repo.get('default_branch', 'main')}\n"
info += f"Clone URL: {self.selected_repo['clone_url']}\n"
info += f"Created: {self.selected_repo['created_at']}\n"
info += f"Updated: {self.selected_repo['updated_at']}\n"
self.info_text.delete(1.0, tk.END)
self.info_text.insert(1.0, info)
def clone_repository(self):
if not self.selected_repo:
messagebox.showwarning("Warning", "Please select a repository")
return
def clone():
try:
success, path = self.repo_manager.clone_repository(self.selected_repo['name'])
if success:
self.selected_repo_path = path
self.parent.after(0, lambda: self.git_output.insert(tk.END, f"Repository cloned to: {path}\n"))
else:
self.parent.after(0, lambda: self.git_output.insert(tk.END, f"Failed to clone repository\n"))
except Exception as e:
self.parent.after(0, lambda: messagebox.showerror("Error", f"Clone failed: {e}"))
threading.Thread(target=clone, daemon=True).start()
def fetch_repository(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
success, result = self.repo_manager.git_ops.fetch(self.selected_repo_path)
self.git_output.insert(tk.END, f"Fetch: {result}\n")
def pull_repository(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
success, result = self.repo_manager.git_ops.pull(self.selected_repo_path)
self.git_output.insert(tk.END, f"Pull: {result}\n")
def push_repository(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
success, result = self.repo_manager.git_ops.push(self.selected_repo_path)
self.git_output.insert(tk.END, f"Push: {result}\n")
def show_status(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
status = self.repo_manager.get_repository_status(self.selected_repo_path)
self.git_output.insert(tk.END, f"\n--- Repository Status ---\n")
self.git_output.insert(tk.END, f"Current Branch: {status['current_branch']}\n")
self.git_output.insert(tk.END, f"Has Changes: {status['has_changes']}\n")
self.git_output.insert(tk.END, f"Status:\n{status['status']}\n")
self.git_output.insert(tk.END, f"Remotes:\n{status['remotes']}\n")
def add_all_files(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
success, result = self.repo_manager.git_ops.add(self.selected_repo_path)
self.git_output.insert(tk.END, f"Add all: {result}\n")
def commit_dialog(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
dialog = tk.Toplevel(self.parent)
dialog.title("Commit Changes")
dialog.geometry("400x200")
ttk.Label(dialog, text="Commit Message:").pack(pady=5)
message_text = tk.Text(dialog, height=5, width=50)
message_text.pack(padx=10, pady=5)
def do_commit():
message = message_text.get(1.0, tk.END).strip()
if not message:
messagebox.showwarning("Warning", "Please enter a commit message")
return
success, result = self.repo_manager.git_ops.commit(self.selected_repo_path, message)
self.git_output.insert(tk.END, f"Commit: {result}\n")
dialog.destroy()
ttk.Button(dialog, text="Commit", command=do_commit).pack(pady=10)
def show_branches(self):
if not self.selected_repo_path:
messagebox.showwarning("Warning", "Please clone the repository first")
return
success, branches = self.repo_manager.git_ops.branch(self.selected_repo_path, list_all=True)
self.git_output.insert(tk.END, f"\n--- Branches ---\n{branches}\n")
def create_repository_dialog(self):
dialog = tk.Toplevel(self.parent)
dialog.title("Create New Repository")
dialog.geometry("400x300")
ttk.Label(dialog, text="Repository Name:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
name_entry = ttk.Entry(dialog, width=30)
name_entry.grid(row=0, column=1, padx=10, pady=5)
ttk.Label(dialog, text="Description:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=5)
desc_text = tk.Text(dialog, height=3, width=30)
desc_text.grid(row=1, column=1, padx=10, pady=5)
private_var = tk.BooleanVar()
ttk.Checkbutton(dialog, text="Private Repository", variable=private_var).grid(row=2, column=1, sticky=tk.W, padx=10, pady=5)
auto_init_var = tk.BooleanVar(value=True)
ttk.Checkbutton(dialog, text="Initialize with README", variable=auto_init_var).grid(row=3, column=1, sticky=tk.W, padx=10, pady=5)
def create():
name = name_entry.get().strip()
desc = desc_text.get(1.0, tk.END).strip()
if not name:
messagebox.showwarning("Warning", "Please enter a repository name")
return
try:
self.repo_manager.create_repository(
name, desc, private_var.get(), auto_init_var.get()
)
messagebox.showinfo("Success", f"Repository '{name}' created successfully")
dialog.destroy()
self.refresh_repositories()
except Exception as e:
messagebox.showerror("Error", f"Failed to create repository: {e}")
ttk.Button(dialog, text="Create", command=create).grid(row=4, column=1, pady=20)
def delete_repository(self):
if not self.selected_repo:
messagebox.showwarning("Warning", "Please select a repository")
return
if messagebox.askyesno("Confirm", f"Are you sure you want to delete '{self.selected_repo['name']}'?"):
if self.repo_manager.delete_repository(self.selected_repo['name']):
messagebox.showinfo("Success", "Repository deleted successfully")
self.refresh_repositories()
def refresh_issues(self):
if not self.selected_repo:
return
self.issues_tree.delete(*self.issues_tree.get_children())
try:
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
issues = manager.list_issues()
for issue in issues:
self.issues_tree.insert('', tk.END, values=(
issue['number'],
issue['title'],
issue['state'],
issue['user']['username'],
issue['created_at'][:10]
))
except Exception as e:
print(f"Error fetching issues: {e}")
def refresh_prs(self):
if not self.selected_repo:
return
self.pr_tree.delete(*self.pr_tree.get_children())
try:
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
prs = manager.list_pull_requests()
for pr in prs:
self.pr_tree.insert('', tk.END, values=(
pr['number'],
pr['title'],
pr['state'],
pr['user']['username'],
pr['head']['ref'],
pr['base']['ref']
))
except Exception as e:
print(f"Error fetching PRs: {e}")
def create_issue_dialog(self):
if not self.selected_repo:
messagebox.showwarning("Warning", "Please select a repository")
return
dialog = tk.Toplevel(self.parent)
dialog.title("Create New Issue")
dialog.geometry("500x400")
ttk.Label(dialog, text="Title:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
title_entry = ttk.Entry(dialog, width=50)
title_entry.grid(row=0, column=1, padx=10, pady=5)
ttk.Label(dialog, text="Description:").grid(row=1, column=0, sticky=tk.NW, padx=10, pady=5)
desc_text = tk.Text(dialog, height=10, width=50)
desc_text.grid(row=1, column=1, padx=10, pady=5)
def create():
title = title_entry.get().strip()
body = desc_text.get(1.0, tk.END).strip()
if not title:
messagebox.showwarning("Warning", "Please enter a title")
return
try:
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
manager.create_issue(title, body)
messagebox.showinfo("Success", "Issue created successfully")
dialog.destroy()
self.refresh_issues()
except Exception as e:
messagebox.showerror("Error", f"Failed to create issue: {e}")
ttk.Button(dialog, text="Create", command=create).grid(row=2, column=1, pady=20)
def edit_issue(self):
messagebox.showinfo("Info", "Edit issue functionality coming soon!")
def close_issue(self):
selection = self.issues_tree.selection()
if not selection:
messagebox.showwarning("Warning", "Please select an issue")
return
item = self.issues_tree.item(selection[0])
issue_id = item['values'][0]
if messagebox.askyesno("Confirm", f"Close issue #{issue_id}?"):
try:
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
manager.close_issue(issue_id)
messagebox.showinfo("Success", "Issue closed successfully")
self.refresh_issues()
except Exception as e:
messagebox.showerror("Error", f"Failed to close issue: {e}")
def create_pr_dialog(self):
if not self.selected_repo:
messagebox.showwarning("Warning", "Please select a repository")
return
dialog = tk.Toplevel(self.parent)
dialog.title("Create Pull Request")
dialog.geometry("500x500")
ttk.Label(dialog, text="Title:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
title_entry = ttk.Entry(dialog, width=50)
title_entry.grid(row=0, column=1, padx=10, pady=5)
ttk.Label(dialog, text="Head Branch:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=5)
head_entry = ttk.Entry(dialog, width=50)
head_entry.grid(row=1, column=1, padx=10, pady=5)
ttk.Label(dialog, text="Base Branch:").grid(row=2, column=0, sticky=tk.W, padx=10, pady=5)
base_entry = ttk.Entry(dialog, width=50, )
base_entry.insert(0, "main")
base_entry.grid(row=2, column=1, padx=10, pady=5)
ttk.Label(dialog, text="Description:").grid(row=3, column=0, sticky=tk.NW, padx=10, pady=5)
desc_text = tk.Text(dialog, height=10, width=50)
desc_text.grid(row=3, column=1, padx=10, pady=5)
def create():
title = title_entry.get().strip()
head = head_entry.get().strip()
base = base_entry.get().strip()
body = desc_text.get(1.0, tk.END).strip()
if not all([title, head, base]):
messagebox.showwarning("Warning", "Please fill all required fields")
return
try:
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
manager.create_pull_request(title, head, base, body)
messagebox.showinfo("Success", "Pull request created successfully")
dialog.destroy()
self.refresh_prs()
except Exception as e:
messagebox.showerror("Error", f"Failed to create PR: {e}")
ttk.Button(dialog, text="Create", command=create).grid(row=4, column=1, pady=20)
def merge_pr(self):
selection = self.pr_tree.selection()
if not selection:
messagebox.showwarning("Warning", "Please select a pull request")
return
item = self.pr_tree.item(selection[0])
pr_id = item['values'][0]
if messagebox.askyesno("Confirm", f"Merge pull request #{pr_id}?"):
try:
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
manager.merge_pull_request(pr_id)
messagebox.showinfo("Success", "Pull request merged successfully")
self.refresh_prs()
except Exception as e:
messagebox.showerror("Error", f"Failed to merge PR: {e}")
def close_pr(self):
selection = self.pr_tree.selection()
if not selection:
messagebox.showwarning("Warning", "Please select a pull request")
return
item = self.pr_tree.item(selection[0])
pr_id = item['values'][0]
if messagebox.askyesno("Confirm", f"Close pull request #{pr_id}?"):
try:
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
manager.close_pull_request(pr_id)
messagebox.showinfo("Success", "Pull request closed successfully")
self.refresh_prs()
except Exception as e:
messagebox.showerror("Error", f"Failed to close PR: {e}")

984
src/gitea/gitea_ui_ctk.py Normale Datei
Datei anzeigen

@ -0,0 +1,984 @@
import customtkinter as ctk
from tkinter import messagebox, filedialog
import threading
from pathlib import Path
from typing import Optional, Dict, Any, List
from .repository_manager import RepositoryManager
from .issue_pr_manager import IssuePRManager
class GiteaIntegrationUI:
def __init__(self, parent_window):
self.window = parent_window
self.repo_manager = RepositoryManager()
self.issue_pr_manager = IssuePRManager(self.repo_manager.client)
self.selected_repo = None
self.selected_repo_path = None
# Import styles from main app
from gui.styles import COLORS, FONTS
self.COLORS = COLORS
self.FONTS = FONTS
self.setup_ui()
self.refresh_repositories()
def setup_ui(self):
# Main container
main_frame = ctk.CTkFrame(self.window, fg_color=self.COLORS['bg_primary'])
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Left panel - Repository list
left_frame = ctk.CTkFrame(main_frame, width=300, fg_color=self.COLORS['bg_secondary'])
left_frame.pack(side="left", fill="y", padx=(0, 10))
left_frame.pack_propagate(False)
# Repository list header
header_frame = ctk.CTkFrame(left_frame, fg_color="transparent", height=50)
header_frame.pack(fill="x", padx=10, pady=(10, 0))
header_frame.pack_propagate(False)
ctk.CTkLabel(
header_frame,
text="Repositories",
font=self.FONTS['title'],
text_color=self.COLORS['text_primary']
).pack(side="left", pady=10)
# Buttons
button_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
button_frame.pack(fill="x", padx=10, pady=5)
ctk.CTkButton(
button_frame,
text="🔄 Refresh",
command=self.refresh_repositories,
width=100,
height=30,
fg_color=self.COLORS['accent_primary'],
hover_color=self.COLORS['accent_hover']
).pack(side="left", padx=2)
ctk.CTkButton(
button_frame,
text=" New Repo",
command=self.create_repository_dialog,
width=100,
height=30,
fg_color=self.COLORS['accent_success'],
hover_color=self.COLORS['accent_hover']
).pack(side="left", padx=2)
ctk.CTkButton(
button_frame,
text="🗑️ Delete",
command=self.delete_repository,
width=100,
height=30,
fg_color=self.COLORS['accent_error'],
hover_color=self.COLORS['accent_hover']
).pack(side="left", padx=2)
ctk.CTkButton(
button_frame,
text="📤 Push Local",
command=self.push_local_repo_dialog,
width=100,
height=30,
fg_color=self.COLORS['accent_warning'],
hover_color=self.COLORS['accent_hover']
).pack(side="left", padx=2)
# Repository list
self.repo_list_frame = ctk.CTkScrollableFrame(
left_frame,
fg_color=self.COLORS['bg_primary']
)
self.repo_list_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
# Right panel - Tab view
right_frame = ctk.CTkFrame(main_frame, fg_color=self.COLORS['bg_secondary'])
right_frame.pack(side="right", fill="both", expand=True)
self.tabview = ctk.CTkTabview(right_frame, fg_color=self.COLORS['bg_secondary'])
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
# Create tabs
self.tabview.add("Git Operations")
self.tabview.add("Issues")
self.tabview.add("Pull Requests")
self.tabview.add("Repository Info")
self.setup_git_operations_tab()
self.setup_issues_tab()
self.setup_pr_tab()
self.setup_info_tab()
def setup_git_operations_tab(self):
tab = self.tabview.tab("Git Operations")
# Operations buttons
operations_frame = ctk.CTkFrame(tab, fg_color=self.COLORS['bg_tile'])
operations_frame.pack(fill="x", padx=10, pady=10)
# Row 1
row1 = ctk.CTkFrame(operations_frame, fg_color="transparent")
row1.pack(fill="x", padx=10, pady=5)
ctk.CTkButton(
row1, text="📥 Clone", command=self.clone_repository,
fg_color=self.COLORS['accent_primary'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
row1, text="🔄 Fetch", command=self.fetch_repository,
fg_color=self.COLORS['accent_primary'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
row1, text="⬇️ Pull", command=self.pull_repository,
fg_color=self.COLORS['accent_primary'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
row1, text="📊 Status", command=self.show_status,
fg_color=self.COLORS['accent_secondary'], width=100
).pack(side="left", padx=5)
# Row 2
row2 = ctk.CTkFrame(operations_frame, fg_color="transparent")
row2.pack(fill="x", padx=10, pady=5)
ctk.CTkButton(
row2, text=" Add All", command=self.add_all_files,
fg_color=self.COLORS['accent_success'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
row2, text="💾 Commit", command=self.commit_dialog,
fg_color=self.COLORS['accent_success'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
row2, text="⬆️ Push", command=self.push_repository,
fg_color=self.COLORS['accent_success'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
row2, text="🌿 Branches", command=self.show_branches,
fg_color=self.COLORS['accent_secondary'], width=100
).pack(side="left", padx=5)
# Output area
output_label = ctk.CTkLabel(
tab, text="Output:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
)
output_label.pack(anchor="w", padx=10, pady=(10, 5))
self.git_output = ctk.CTkTextbox(
tab,
fg_color=self.COLORS['bg_primary'],
text_color=self.COLORS['text_primary'],
font=self.FONTS['code']
)
self.git_output.pack(fill="both", expand=True, padx=10, pady=(0, 10))
def setup_issues_tab(self):
tab = self.tabview.tab("Issues")
# Issue buttons
button_frame = ctk.CTkFrame(tab, fg_color="transparent")
button_frame.pack(fill="x", padx=10, pady=10)
ctk.CTkButton(
button_frame, text="🔄 Refresh", command=self.refresh_issues,
fg_color=self.COLORS['accent_primary'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
button_frame, text=" New Issue", command=self.create_issue_dialog,
fg_color=self.COLORS['accent_success'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
button_frame, text="✅ Close", command=self.close_issue,
fg_color=self.COLORS['accent_error'], width=100
).pack(side="left", padx=5)
# Issues list
self.issues_frame = ctk.CTkScrollableFrame(
tab,
fg_color=self.COLORS['bg_primary']
)
self.issues_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
self.issue_widgets = []
def setup_pr_tab(self):
tab = self.tabview.tab("Pull Requests")
# PR buttons
button_frame = ctk.CTkFrame(tab, fg_color="transparent")
button_frame.pack(fill="x", padx=10, pady=10)
ctk.CTkButton(
button_frame, text="🔄 Refresh", command=self.refresh_prs,
fg_color=self.COLORS['accent_primary'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
button_frame, text=" New PR", command=self.create_pr_dialog,
fg_color=self.COLORS['accent_success'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
button_frame, text="🔀 Merge", command=self.merge_pr,
fg_color=self.COLORS['accent_success'], width=100
).pack(side="left", padx=5)
ctk.CTkButton(
button_frame, text="❌ Close", command=self.close_pr,
fg_color=self.COLORS['accent_error'], width=100
).pack(side="left", padx=5)
# PR list
self.pr_frame = ctk.CTkScrollableFrame(
tab,
fg_color=self.COLORS['bg_primary']
)
self.pr_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
self.pr_widgets = []
def setup_info_tab(self):
tab = self.tabview.tab("Repository Info")
self.info_text = ctk.CTkTextbox(
tab,
fg_color=self.COLORS['bg_primary'],
text_color=self.COLORS['text_primary'],
font=self.FONTS['body']
)
self.info_text.pack(fill="both", expand=True, padx=10, pady=10)
def refresh_repositories(self):
def fetch():
try:
repos = self.repo_manager.list_all_repositories()
self.window.after(0, lambda: self.update_repo_list(repos))
except Exception as e:
self.window.after(0, lambda: self.show_error(f"Failed to fetch repositories: {e}"))
threading.Thread(target=fetch, daemon=True).start()
def update_repo_list(self, repos):
# Clear existing
for widget in self.repo_list_frame.winfo_children():
widget.destroy()
self.repo_buttons = {}
self.repositories = repos
for repo in repos:
btn = ctk.CTkButton(
self.repo_list_frame,
text=f"{'🔒' if repo['private'] else '🌐'} {repo['name']}",
command=lambda r=repo: self.select_repository(r),
fg_color=self.COLORS['bg_tile'],
hover_color=self.COLORS['bg_tile_hover'],
text_color=self.COLORS['text_primary'],
anchor="w"
)
btn.pack(fill="x", pady=2)
self.repo_buttons[repo['id']] = btn
def select_repository(self, repo):
self.selected_repo = repo
# Update button colors
for repo_id, btn in self.repo_buttons.items():
if repo_id == repo['id']:
btn.configure(fg_color=self.COLORS['accent_primary'])
else:
btn.configure(fg_color=self.COLORS['bg_tile'])
self.update_repo_info()
self.refresh_issues()
self.refresh_prs()
def update_repo_info(self):
if not self.selected_repo:
return
info = f"Repository: {self.selected_repo['name']}\n"
info += f"Description: {self.selected_repo.get('description', 'No description')}\n"
info += f"Private: {'Yes' if self.selected_repo['private'] else 'No'}\n"
info += f"Default Branch: {self.selected_repo.get('default_branch', 'main')}\n"
info += f"Clone URL: {self.selected_repo['clone_url']}\n"
info += f"Created: {self.selected_repo['created_at']}\n"
info += f"Updated: {self.selected_repo['updated_at']}\n"
self.info_text.delete("0.0", "end")
self.info_text.insert("0.0", info)
def clone_repository(self):
if not self.selected_repo:
self.show_warning("Please select a repository")
return
def clone():
try:
success, path = self.repo_manager.clone_repository(self.selected_repo['name'])
if success:
self.selected_repo_path = path
self.window.after(0, lambda: self.append_output(f"Repository cloned to: {path}\n"))
else:
self.window.after(0, lambda: self.append_output(f"Failed to clone repository\n"))
except Exception as e:
self.window.after(0, lambda: self.show_error(f"Clone failed: {e}"))
threading.Thread(target=clone, daemon=True).start()
def fetch_repository(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
success, result = self.repo_manager.git_ops.fetch(self.selected_repo_path)
self.append_output(f"Fetch: {result}\n")
def pull_repository(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
success, result = self.repo_manager.git_ops.pull(self.selected_repo_path)
self.append_output(f"Pull: {result}\n")
def push_repository(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
success, result = self.repo_manager.git_ops.push(self.selected_repo_path)
self.append_output(f"Push: {result}\n")
def show_status(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
status = self.repo_manager.get_repository_status(self.selected_repo_path)
self.append_output(f"\n--- Repository Status ---\n")
self.append_output(f"Current Branch: {status['current_branch']}\n")
self.append_output(f"Has Changes: {status['has_changes']}\n")
self.append_output(f"Status:\n{status['status']}\n")
self.append_output(f"Remotes:\n{status['remotes']}\n")
def add_all_files(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
success, result = self.repo_manager.git_ops.add(self.selected_repo_path)
self.append_output(f"Add all: {result}\n")
def commit_dialog(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
dialog = ctk.CTkToplevel(self.window)
dialog.title("Commit Changes")
dialog.geometry("500x300")
dialog.configure(fg_color=self.COLORS['bg_primary'])
# Center dialog
dialog.transient(self.window)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 500) // 2
y = (dialog.winfo_screenheight() - 300) // 2
dialog.geometry(f"500x300+{x}+{y}")
ctk.CTkLabel(
dialog,
text="Commit Message:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 10))
message_text = ctk.CTkTextbox(
dialog,
height=150,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
message_text.pack(padx=20, pady=10, fill="both", expand=True)
def do_commit():
message = message_text.get("0.0", "end").strip()
if not message:
self.show_warning("Please enter a commit message")
return
success, result = self.repo_manager.git_ops.commit(self.selected_repo_path, message)
self.append_output(f"Commit: {result}\n")
dialog.destroy()
ctk.CTkButton(
dialog,
text="Commit",
command=do_commit,
fg_color=self.COLORS['accent_success']
).pack(pady=20)
def show_branches(self):
if not self.selected_repo_path:
self.show_warning("Please clone the repository first")
return
success, branches = self.repo_manager.git_ops.branch(self.selected_repo_path, list_all=True)
self.append_output(f"\n--- Branches ---\n{branches}\n")
def create_repository_dialog(self):
dialog = ctk.CTkToplevel(self.window)
dialog.title("Create New Repository")
dialog.geometry("500x400")
dialog.configure(fg_color=self.COLORS['bg_primary'])
# Center dialog
dialog.transient(self.window)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 500) // 2
y = (dialog.winfo_screenheight() - 400) // 2
dialog.geometry(f"500x400+{x}+{y}")
# Name
ctk.CTkLabel(
dialog, text="Repository Name:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
name_entry = ctk.CTkEntry(
dialog, width=400,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
name_entry.pack(pady=5)
# Description
ctk.CTkLabel(
dialog, text="Description:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
desc_text = ctk.CTkTextbox(
dialog, height=100, width=400,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
desc_text.pack(pady=5)
# Options
private_var = ctk.BooleanVar()
ctk.CTkCheckBox(
dialog, text="Private Repository",
variable=private_var,
text_color=self.COLORS['text_primary']
).pack(pady=10)
auto_init_var = ctk.BooleanVar(value=True)
ctk.CTkCheckBox(
dialog, text="Initialize with README",
variable=auto_init_var,
text_color=self.COLORS['text_primary']
).pack(pady=5)
def create():
name = name_entry.get().strip()
desc = desc_text.get("0.0", "end").strip()
if not name:
self.show_warning("Please enter a repository name")
return
try:
self.repo_manager.create_repository(
name, desc, private_var.get(), auto_init_var.get()
)
self.show_info(f"Repository '{name}' created successfully")
dialog.destroy()
self.refresh_repositories()
except Exception as e:
self.show_error(f"Failed to create repository: {e}")
ctk.CTkButton(
dialog, text="Create", command=create,
fg_color=self.COLORS['accent_success']
).pack(pady=20)
def delete_repository(self):
if not self.selected_repo:
self.show_warning("Please select a repository")
return
if messagebox.askyesno("Confirm", f"Are you sure you want to delete '{self.selected_repo['name']}'?"):
if self.repo_manager.delete_repository(self.selected_repo['name']):
self.show_info("Repository deleted successfully")
self.refresh_repositories()
def refresh_issues(self):
if not self.selected_repo:
return
# Clear existing
for widget in self.issue_widgets:
widget.destroy()
self.issue_widgets.clear()
try:
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
issues = manager.list_issues()
for issue in issues:
frame = ctk.CTkFrame(self.issues_frame, fg_color=self.COLORS['bg_tile'])
frame.pack(fill="x", pady=5)
# Issue info
info_frame = ctk.CTkFrame(frame, fg_color="transparent")
info_frame.pack(fill="x", padx=10, pady=10)
ctk.CTkLabel(
info_frame,
text=f"#{issue['number']}: {issue['title']}",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(anchor="w")
ctk.CTkLabel(
info_frame,
text=f"State: {issue['state']} | Author: {issue['user']['username']} | Created: {issue['created_at'][:10]}",
font=self.FONTS['small'],
text_color=self.COLORS['text_secondary']
).pack(anchor="w")
self.issue_widgets.append(frame)
except Exception as e:
print(f"Error fetching issues: {e}")
def refresh_prs(self):
if not self.selected_repo:
return
# Clear existing
for widget in self.pr_widgets:
widget.destroy()
self.pr_widgets.clear()
try:
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
prs = manager.list_pull_requests()
for pr in prs:
frame = ctk.CTkFrame(self.pr_frame, fg_color=self.COLORS['bg_tile'])
frame.pack(fill="x", pady=5)
# PR info
info_frame = ctk.CTkFrame(frame, fg_color="transparent")
info_frame.pack(fill="x", padx=10, pady=10)
ctk.CTkLabel(
info_frame,
text=f"#{pr['number']}: {pr['title']}",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(anchor="w")
ctk.CTkLabel(
info_frame,
text=f"State: {pr['state']} | {pr['head']['ref']}{pr['base']['ref']} | Author: {pr['user']['username']}",
font=self.FONTS['small'],
text_color=self.COLORS['text_secondary']
).pack(anchor="w")
self.pr_widgets.append(frame)
except Exception as e:
print(f"Error fetching PRs: {e}")
def create_issue_dialog(self):
if not self.selected_repo:
self.show_warning("Please select a repository")
return
dialog = ctk.CTkToplevel(self.window)
dialog.title("Create New Issue")
dialog.geometry("600x500")
dialog.configure(fg_color=self.COLORS['bg_primary'])
# Center dialog
dialog.transient(self.window)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 500) // 2
dialog.geometry(f"600x500+{x}+{y}")
# Title
ctk.CTkLabel(
dialog, text="Title:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
title_entry = ctk.CTkEntry(
dialog, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
title_entry.pack(pady=5)
# Description
ctk.CTkLabel(
dialog, text="Description:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
desc_text = ctk.CTkTextbox(
dialog, height=250, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
desc_text.pack(pady=5)
def create():
title = title_entry.get().strip()
body = desc_text.get("0.0", "end").strip()
if not title:
self.show_warning("Please enter a title")
return
try:
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
manager.create_issue(title, body)
self.show_info("Issue created successfully")
dialog.destroy()
self.refresh_issues()
except Exception as e:
self.show_error(f"Failed to create issue: {e}")
ctk.CTkButton(
dialog, text="Create", command=create,
fg_color=self.COLORS['accent_success']
).pack(pady=20)
def close_issue(self):
# TODO: Need to implement issue selection first
self.show_info("Please select an issue from the list to close")
def create_pr_dialog(self):
if not self.selected_repo:
self.show_warning("Please select a repository")
return
dialog = ctk.CTkToplevel(self.window)
dialog.title("Create Pull Request")
dialog.geometry("600x600")
dialog.configure(fg_color=self.COLORS['bg_primary'])
# Center dialog
dialog.transient(self.window)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 600) // 2
dialog.geometry(f"600x600+{x}+{y}")
# Title
ctk.CTkLabel(
dialog, text="Title:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
title_entry = ctk.CTkEntry(
dialog, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
title_entry.pack(pady=5)
# Branches
ctk.CTkLabel(
dialog, text="Head Branch:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
head_entry = ctk.CTkEntry(
dialog, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
head_entry.pack(pady=5)
ctk.CTkLabel(
dialog, text="Base Branch:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
base_entry = ctk.CTkEntry(
dialog, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
base_entry.insert(0, "main")
base_entry.pack(pady=5)
# Description
ctk.CTkLabel(
dialog, text="Description:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
desc_text = ctk.CTkTextbox(
dialog, height=200, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
desc_text.pack(pady=5)
def create():
title = title_entry.get().strip()
head = head_entry.get().strip()
base = base_entry.get().strip()
body = desc_text.get("0.0", "end").strip()
if not all([title, head, base]):
self.show_warning("Please fill all required fields")
return
try:
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
manager.create_pull_request(title, head, base, body)
self.show_info("Pull request created successfully")
dialog.destroy()
self.refresh_prs()
except Exception as e:
self.show_error(f"Failed to create PR: {e}")
ctk.CTkButton(
dialog, text="Create", command=create,
fg_color=self.COLORS['accent_success']
).pack(pady=20)
def merge_pr(self):
# TODO: Need to implement PR selection first
self.show_info("Please select a pull request from the list to merge")
def close_pr(self):
# TODO: Need to implement PR selection first
self.show_info("Please select a pull request from the list to close")
# Helper methods
def append_output(self, text):
self.git_output.insert("end", text)
self.git_output.see("end")
def show_error(self, message):
messagebox.showerror("Error", message)
def show_warning(self, message):
messagebox.showwarning("Warning", message)
def show_info(self, message):
messagebox.showinfo("Info", message)
def push_local_repo_dialog(self, project_name=None, project_path=None):
"""Dialog to push an existing local repository to Gitea"""
dialog = ctk.CTkToplevel(self.window)
dialog.title("Push Local Repository to Gitea")
dialog.geometry("600x600")
dialog.configure(fg_color=self.COLORS['bg_primary'])
# Center dialog
dialog.transient(self.window)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 600) // 2
dialog.geometry(f"600x600+{x}+{y}")
# Add description label
desc_label = ctk.CTkLabel(
dialog,
text="Push an existing local Git repository to your Gitea server",
font=self.FONTS['body'],
text_color=self.COLORS['text_secondary']
)
desc_label.pack(pady=(10, 5))
# Local repository path
ctk.CTkLabel(
dialog, text="Local Repository Path:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
path_frame = ctk.CTkFrame(dialog, fg_color="transparent")
path_frame.pack(pady=5, padx=20, fill="x")
path_var = ctk.StringVar()
path_entry = ctk.CTkEntry(
path_frame, width=400,
textvariable=path_var,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
path_entry.pack(side="left", padx=(0, 10))
def browse_folder():
from tkinter import filedialog
folder = filedialog.askdirectory()
if folder:
path_var.set(folder)
# Update repo name field with folder name if empty
if not name_entry.get().strip():
import os
folder_name = os.path.basename(folder)
name_entry.delete(0, 'end')
name_entry.insert(0, folder_name)
# Set initial values if provided
if project_path:
path_var.set(project_path)
ctk.CTkButton(
path_frame, text="Browse", command=browse_folder,
fg_color=self.COLORS['accent_primary'], width=100
).pack(side="left")
# Repository name
ctk.CTkLabel(
dialog, text="Repository Name:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
name_entry = ctk.CTkEntry(
dialog, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
name_entry.pack(pady=5, padx=20)
# Set initial name if provided
if project_name:
name_entry.insert(0, project_name)
# Description
ctk.CTkLabel(
dialog, text="Description:",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(20, 5))
desc_text = ctk.CTkTextbox(
dialog, height=100, width=500,
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
desc_text.pack(pady=5, padx=20)
# Options
private_var = ctk.BooleanVar()
ctk.CTkCheckBox(
dialog, text="Private Repository",
variable=private_var,
text_color=self.COLORS['text_primary']
).pack(pady=10)
# Branch name
ctk.CTkLabel(
dialog, text="Branch to push (default: main):",
font=self.FONTS['subtitle'],
text_color=self.COLORS['text_primary']
).pack(pady=(10, 5))
branch_entry = ctk.CTkEntry(
dialog, width=200,
placeholder_text="main",
fg_color=self.COLORS['bg_secondary'],
text_color=self.COLORS['text_primary']
)
branch_entry.pack(pady=5)
# Status label for feedback
status_label = ctk.CTkLabel(
dialog, text="",
font=self.FONTS['body'],
text_color=self.COLORS['text_secondary']
)
status_label.pack(pady=10)
def push_repo():
local_path = path_var.get().strip()
repo_name = name_entry.get().strip()
desc = desc_text.get("0.0", "end").strip()
branch = branch_entry.get().strip() or "main"
if not local_path:
self.show_warning("Please select a local repository path")
return
if not repo_name:
self.show_warning("Please enter a repository name")
return
# Check if it's a valid git repository
from pathlib import Path
repo_path = Path(local_path)
if not (repo_path / ".git").exists():
self.show_warning("Selected path is not a git repository")
return
def do_push():
try:
# Update status
self.window.after(0, lambda: status_label.configure(
text="Creating repository on Gitea...",
text_color=self.COLORS['accent_primary']
))
success, message = self.repo_manager.push_local_repo_to_gitea(
repo_path, repo_name, desc, private_var.get(), branch
)
if success:
self.window.after(0, lambda: self.show_info(message))
self.window.after(0, dialog.destroy)
self.window.after(0, self.refresh_repositories)
else:
self.window.after(0, lambda: status_label.configure(
text=f"Error: {message}",
text_color=self.COLORS['accent_error']
))
except Exception as e:
self.window.after(0, lambda: status_label.configure(
text=f"Error: {str(e)}",
text_color=self.COLORS['accent_error']
))
# Run in thread to avoid blocking UI
import threading
threading.Thread(target=do_push, daemon=True).start()
ctk.CTkButton(
dialog, text="Push to Gitea", command=push_repo,
fg_color=self.COLORS['accent_success']
).pack(pady=20)

299
src/gitea/issue_pr_manager.py Normale Datei
Datei anzeigen

@ -0,0 +1,299 @@
import logging
from typing import List, Dict, Any, Optional
from datetime import datetime
from .gitea_client import GiteaClient
logger = logging.getLogger(__name__)
class IssueManager:
def __init__(self, gitea_client: GiteaClient, owner: str, repo: str):
self.client = gitea_client
self.owner = owner
self.repo = repo
def list_issues(self, state: str = "open", labels: Optional[List[str]] = None) -> List[Dict[str, Any]]:
all_issues = []
page = 1
while True:
issues = self.client.list_issues(
self.owner, self.repo, state=state, labels=labels, page=page
)
if not issues:
break
all_issues.extend(issues)
page += 1
return all_issues
def create_issue(self, title: str, body: str = "",
assignees: Optional[List[str]] = None,
labels: Optional[List[int]] = None) -> Optional[Dict[str, Any]]:
try:
issue = self.client.create_issue(
self.owner, self.repo, title, body, assignees, labels
)
logger.info(f"Issue '{title}' created successfully")
return issue
except Exception as e:
logger.error(f"Failed to create issue: {e}")
return None
def update_issue(self, index: int, **kwargs) -> Optional[Dict[str, Any]]:
try:
issue = self.client.update_issue(self.owner, self.repo, index, **kwargs)
logger.info(f"Issue #{index} updated successfully")
return issue
except Exception as e:
logger.error(f"Failed to update issue #{index}: {e}")
return None
def close_issue(self, index: int) -> bool:
try:
self.client.close_issue(self.owner, self.repo, index)
logger.info(f"Issue #{index} closed successfully")
return True
except Exception as e:
logger.error(f"Failed to close issue #{index}: {e}")
return False
def add_comment(self, index: int, comment: str) -> Optional[Dict[str, Any]]:
try:
data = {"body": comment}
comment_data = self.client._request(
"POST",
f"repos/{self.owner}/{self.repo}/issues/{index}/comments",
json=data
)
logger.info(f"Comment added to issue #{index}")
return comment_data
except Exception as e:
logger.error(f"Failed to add comment to issue #{index}: {e}")
return None
def get_issue_comments(self, index: int) -> List[Dict[str, Any]]:
try:
return self.client._request(
"GET",
f"repos/{self.owner}/{self.repo}/issues/{index}/comments"
)
except Exception as e:
logger.error(f"Failed to get comments for issue #{index}: {e}")
return []
def search_issues(self, query: str) -> List[Dict[str, Any]]:
all_issues = self.list_issues(state="all")
query_lower = query.lower()
return [
issue for issue in all_issues
if query_lower in issue["title"].lower() or
query_lower in issue.get("body", "").lower()
]
class PullRequestManager:
def __init__(self, gitea_client: GiteaClient, owner: str, repo: str):
self.client = gitea_client
self.owner = owner
self.repo = repo
def list_pull_requests(self, state: str = "open") -> List[Dict[str, Any]]:
all_prs = []
page = 1
while True:
prs = self.client.list_pull_requests(
self.owner, self.repo, state=state, page=page
)
if not prs:
break
all_prs.extend(prs)
page += 1
return all_prs
def create_pull_request(self, title: str, head: str, base: str,
body: str = "", assignees: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
try:
pr = self.client.create_pull_request(
self.owner, self.repo, title, head, base, body, assignees
)
logger.info(f"Pull request '{title}' created successfully")
return pr
except Exception as e:
logger.error(f"Failed to create pull request: {e}")
return None
def update_pull_request(self, index: int, **kwargs) -> Optional[Dict[str, Any]]:
try:
pr = self.client._request(
"PATCH",
f"repos/{self.owner}/{self.repo}/pulls/{index}",
json=kwargs
)
logger.info(f"Pull request #{index} updated successfully")
return pr
except Exception as e:
logger.error(f"Failed to update pull request #{index}: {e}")
return None
def merge_pull_request(self, index: int, merge_style: str = "merge") -> bool:
try:
self.client.merge_pull_request(self.owner, self.repo, index, merge_style)
logger.info(f"Pull request #{index} merged successfully")
return True
except Exception as e:
logger.error(f"Failed to merge pull request #{index}: {e}")
return False
def close_pull_request(self, index: int) -> bool:
return self.update_pull_request(index, state="closed") is not None
def get_pull_request_diff(self, index: int) -> Optional[str]:
try:
response = self.client.session.get(
f"{self.client.config.api_url}/repos/{self.owner}/{self.repo}/pulls/{index}.diff",
headers={"Accept": "text/plain"}
)
response.raise_for_status()
return response.text
except Exception as e:
logger.error(f"Failed to get diff for PR #{index}: {e}")
return None
def get_pull_request_commits(self, index: int) -> List[Dict[str, Any]]:
try:
return self.client._request(
"GET",
f"repos/{self.owner}/{self.repo}/pulls/{index}/commits"
)
except Exception as e:
logger.error(f"Failed to get commits for PR #{index}: {e}")
return []
def add_comment(self, index: int, comment: str) -> Optional[Dict[str, Any]]:
try:
data = {"body": comment}
comment_data = self.client._request(
"POST",
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews",
json=data
)
logger.info(f"Comment added to pull request #{index}")
return comment_data
except Exception as e:
logger.error(f"Failed to add comment to PR #{index}: {e}")
return None
def get_pull_request_reviews(self, index: int) -> List[Dict[str, Any]]:
try:
return self.client._request(
"GET",
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews"
)
except Exception as e:
logger.error(f"Failed to get reviews for PR #{index}: {e}")
return []
def approve_pull_request(self, index: int, comment: str = "") -> Optional[Dict[str, Any]]:
try:
data = {
"body": comment,
"event": "APPROVE"
}
review = self.client._request(
"POST",
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews",
json=data
)
logger.info(f"Pull request #{index} approved")
return review
except Exception as e:
logger.error(f"Failed to approve PR #{index}: {e}")
return None
def request_changes(self, index: int, comment: str) -> Optional[Dict[str, Any]]:
try:
data = {
"body": comment,
"event": "REQUEST_CHANGES"
}
review = self.client._request(
"POST",
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews",
json=data
)
logger.info(f"Changes requested for PR #{index}")
return review
except Exception as e:
logger.error(f"Failed to request changes for PR #{index}: {e}")
return None
class IssuePRManager:
def __init__(self, gitea_client: Optional[GiteaClient] = None):
self.client = gitea_client or GiteaClient()
self._current_user = None
self._issue_managers = {}
self._pr_managers = {}
@property
def current_user(self) -> Dict[str, Any]:
if self._current_user is None:
self._current_user = self.client.get_user_info()
return self._current_user
def get_issue_manager(self, repo_name: str) -> IssueManager:
if repo_name not in self._issue_managers:
owner = self.current_user["username"]
self._issue_managers[repo_name] = IssueManager(self.client, owner, repo_name)
return self._issue_managers[repo_name]
def get_pr_manager(self, repo_name: str) -> PullRequestManager:
if repo_name not in self._pr_managers:
owner = self.current_user["username"]
self._pr_managers[repo_name] = PullRequestManager(self.client, owner, repo_name)
return self._pr_managers[repo_name]
def list_all_issues(self, state: str = "open") -> Dict[str, List[Dict[str, Any]]]:
repos = self.client.list_repositories()
all_issues = {}
for repo in repos:
repo_name = repo["name"]
manager = self.get_issue_manager(repo_name)
issues = manager.list_issues(state=state)
if issues:
all_issues[repo_name] = issues
return all_issues
def list_all_pull_requests(self, state: str = "open") -> Dict[str, List[Dict[str, Any]]]:
repos = self.client.list_repositories()
all_prs = {}
for repo in repos:
repo_name = repo["name"]
manager = self.get_pr_manager(repo_name)
prs = manager.list_pull_requests(state=state)
if prs:
all_prs[repo_name] = prs
return all_prs
def get_activity_summary(self) -> Dict[str, Any]:
open_issues = self.list_all_issues("open")
open_prs = self.list_all_pull_requests("open")
total_issues = sum(len(issues) for issues in open_issues.values())
total_prs = sum(len(prs) for prs in open_prs.values())
return {
"open_issues": total_issues,
"open_pull_requests": total_prs,
"repositories_with_issues": len(open_issues),
"repositories_with_prs": len(open_prs),
"details": {
"issues_by_repo": {repo: len(issues) for repo, issues in open_issues.items()},
"prs_by_repo": {repo: len(prs) for repo, prs in open_prs.items()}
}
}

Datei anzeigen

@ -0,0 +1,274 @@
import logging
from typing import List, Dict, Any, Optional, Tuple
from pathlib import Path
from .gitea_client import GiteaClient, GiteaConfig
from .git_operations import GitOperationsManager
logger = logging.getLogger(__name__)
class RepositoryManager:
def __init__(self, gitea_client: Optional[GiteaClient] = None):
self.client = gitea_client or GiteaClient()
self._current_user = None
try:
# Get the actual username from Gitea
user_info = self.client.get_user_info()
actual_username = user_info.get('username', self.client.config.username)
self._current_user = user_info
except Exception as e:
logger.warning(f"Failed to get user info from Gitea: {e}")
# Fall back to config username
actual_username = self.client.config.username
self.git_ops = GitOperationsManager(
base_url=self.client.config.base_url,
token=self.client.config.api_token,
username=actual_username
)
@property
def current_user(self) -> Dict[str, Any]:
if self._current_user is None:
self._current_user = self.client.get_user_info()
return self._current_user
def list_all_repositories(self) -> List[Dict[str, Any]]:
all_repos = []
page = 1
while True:
repos = self.client.list_repositories(page=page)
if not repos:
break
all_repos.extend(repos)
page += 1
return all_repos
def list_organization_repositories(self, org_name: str, page: int = None, per_page: int = None) -> List[Dict[str, Any]]:
"""List all repositories for an organization"""
if page is not None and per_page is not None:
# Single page request
try:
repos = self.client._request("GET", f"orgs/{org_name}/repos", params={"page": page, "limit": per_page})
return repos if repos else []
except Exception as e:
logger.error(f"Failed to list org repositories: {e}")
return []
else:
# Get all pages
all_repos = []
current_page = 1
while True:
try:
repos = self.client._request("GET", f"orgs/{org_name}/repos", params={"page": current_page, "limit": 50})
if not repos:
break
all_repos.extend(repos)
current_page += 1
except Exception as e:
logger.error(f"Failed to list org repositories: {e}")
break
return all_repos
def create_repository(self, name: str, description: str = "", private: bool = False,
auto_init: bool = True, gitignore: str = "", license: str = "",
organization: str = None) -> Dict[str, Any]:
try:
# Try to create in organization first
if organization:
try:
data = {
"name": name,
"description": description,
"private": private,
"auto_init": auto_init,
"gitignores": gitignore,
"license": license,
"default_branch": "main" # Use main instead of master
}
repo = self.client._request("POST", f"orgs/{organization}/repos", json=data)
logger.info(f"Repository '{name}' created successfully in organization '{organization}'")
return repo
except Exception as org_error:
logger.error(f"Failed to create in organization {organization}: {org_error}")
# Don't fall back - raise the error so user knows what happened
raise Exception(f"Konnte Repository nicht in Organisation '{organization}' erstellen: {str(org_error)}")
# Create as user repository
repo = self.client.create_repository(
name=name,
description=description,
private=private,
auto_init=auto_init,
gitignores=gitignore,
license=license
)
logger.info(f"Repository '{name}' created successfully as user repository")
return repo
except Exception as e:
logger.error(f"Failed to create repository '{name}': {e}")
raise
def delete_repository(self, repo_name: str) -> bool:
try:
owner = self.current_user["username"]
self.client.delete_repository(owner, repo_name)
logger.info(f"Repository '{repo_name}' deleted successfully")
return True
except Exception as e:
logger.error(f"Failed to delete repository '{repo_name}': {e}")
return False
def clone_repository(self, repo_name: str, clone_dir: Optional[Path] = None) -> Tuple[bool, Path]:
owner = self.current_user["username"]
return self.git_ops.clone_repository(owner, repo_name, clone_dir)
def get_repository_info(self, repo_name: str) -> Optional[Dict[str, Any]]:
try:
owner = self.current_user["username"]
return self.client.get_repository(owner, repo_name)
except Exception as e:
logger.error(f"Failed to get repository info for '{repo_name}': {e}")
return None
def fork_repository(self, owner: str, repo_name: str) -> Optional[Dict[str, Any]]:
try:
fork = self.client.fork_repository(owner, repo_name)
logger.info(f"Repository '{owner}/{repo_name}' forked successfully")
return fork
except Exception as e:
logger.error(f"Failed to fork repository '{owner}/{repo_name}': {e}")
return None
def search_repositories(self, query: str) -> List[Dict[str, Any]]:
all_repos = self.list_all_repositories()
query_lower = query.lower()
return [
repo for repo in all_repos
if query_lower in repo["name"].lower() or
query_lower in repo.get("description", "").lower()
]
def sync_repository(self, repo_path: Path) -> Tuple[bool, str]:
success, fetch_result = self.git_ops.fetch(repo_path)
if not success:
return False, f"Fetch failed: {fetch_result}"
success, pull_result = self.git_ops.pull(repo_path)
if not success:
return False, f"Pull failed: {pull_result}"
return True, "Repository synchronized successfully"
def commit_and_push(self, repo_path: Path, message: str,
files: Optional[List[str]] = None) -> Tuple[bool, str]:
success, add_result = self.git_ops.add(repo_path, files)
if not success:
return False, f"Add failed: {add_result}"
success, commit_result = self.git_ops.commit(repo_path, message)
if not success:
return False, f"Commit failed: {commit_result}"
success, push_result = self.git_ops.push(repo_path)
if not success:
return False, f"Push failed: {push_result}"
return True, "Changes committed and pushed successfully"
def get_repository_status(self, repo_path: Path) -> Dict[str, Any]:
success, status = self.git_ops.status(repo_path)
success_branch, branches = self.git_ops.branch(repo_path)
success_remote, remotes = self.git_ops.remote_list(repo_path)
current_branch = None
if success_branch:
for line in branches.split('\n'):
if line.startswith('*'):
current_branch = line[2:].strip()
break
return {
"has_changes": bool(status.strip()) if success else None,
"status": status if success else "Unable to get status",
"current_branch": current_branch,
"remotes": remotes if success_remote else "Unable to get remotes"
}
def create_branch(self, repo_path: Path, branch_name: str) -> Tuple[bool, str]:
return self.git_ops.checkout(repo_path, branch_name, create=True)
def switch_branch(self, repo_path: Path, branch_name: str) -> Tuple[bool, str]:
return self.git_ops.checkout(repo_path, branch_name)
def list_branches(self, repo_name: str) -> List[Dict[str, Any]]:
try:
owner = self.current_user["username"]
return self.client.list_branches(owner, repo_name)
except Exception as e:
logger.error(f"Failed to list branches for '{repo_name}': {e}")
return []
def create_remote_branch(self, repo_name: str, branch_name: str,
base_branch: str = "main") -> Optional[Dict[str, Any]]:
try:
owner = self.current_user["username"]
return self.client.create_branch(owner, repo_name, branch_name, base_branch)
except Exception as e:
logger.error(f"Failed to create branch '{branch_name}' in '{repo_name}': {e}")
return None
def delete_remote_branch(self, repo_name: str, branch_name: str) -> bool:
try:
owner = self.current_user["username"]
self.client.delete_branch(owner, repo_name, branch_name)
logger.info(f"Branch '{branch_name}' deleted from '{repo_name}'")
return True
except Exception as e:
logger.error(f"Failed to delete branch '{branch_name}' from '{repo_name}': {e}")
return False
def push_local_repo_to_gitea(self, local_repo_path: Path, repo_name: str,
description: str = "", private: bool = False,
branch: str = "main", organization: str = None) -> Tuple[bool, str]:
"""Create a new repo on Gitea and push an existing local repository to it"""
try:
# First create the repository on Gitea
repo = self.create_repository(
name=repo_name,
description=description,
private=private,
auto_init=False, # Important: don't initialize since we're pushing existing code
organization=organization
)
# Determine the correct owner
if organization:
owner = organization
elif 'owner' in repo and repo['owner']:
owner = repo['owner']['username'] if 'username' in repo['owner'] else repo['owner']['login']
else:
owner = self.current_user["username"]
logger.info(f"Repository created, owner determined as: {owner}")
# Then push the local repository
success, result = self.git_ops.push_existing_repo_to_gitea(
local_repo_path, owner, repo_name, branch
)
if success:
logger.info(f"Successfully pushed local repository to '{repo_name}'")
return True, f"Repository '{repo_name}' created and pushed successfully"
else:
logger.error(f"Failed to push to '{repo_name}': {result}")
return False, f"Repository created but push failed: {result}"
except Exception as e:
logger.error(f"Failed to push local repository: {e}")
return False, str(e)

32
start.bat Normale Datei
Datei anzeigen

@ -0,0 +1,32 @@
@echo off
title Claude Project Manager
echo Starting Claude Project Manager...
echo.
:: Check if Python is installed
python --version >nul 2>&1
if errorlevel 1 (
echo ERROR: Python is not installed or not in PATH
echo Please install Python 3.8 or higher
pause
exit /b 1
)
:: Check if requirements are installed
python -c "import customtkinter" >nul 2>&1
if errorlevel 1 (
echo Installing required packages...
pip install -r requirements.txt
echo.
)
:: Start the application
echo Launching application...
python main.py
:: If app crashes, keep window open
if errorlevel 1 (
echo.
echo Application crashed!
pause
)

264
terminal_launcher.py Normale Datei
Datei anzeigen

@ -0,0 +1,264 @@
"""
Terminal Launcher Module
Handles launching Claude in WSL terminal with specified directory
"""
import subprocess
import os
import tempfile
from datetime import datetime
from typing import Optional, Union
from utils.logger import logger
class TerminalLauncher:
def __init__(self):
self.claude_path = self.find_claude_path()
def find_claude_path(self) -> str:
"""Find Claude path dynamically in WSL"""
# Try to find claude using 'which' command in WSL with login shell
try:
# Hide console window on Windows
startupinfo = None
if hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# Use login shell to ensure all PATH and aliases are loaded
command = ["wsl", "bash", "-l", "-c", "which claude 2>/dev/null || command -v claude 2>/dev/null || type -p claude 2>/dev/null"]
logger.debug(f"Executing command to find Claude: {' '.join(command)}")
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=5,
startupinfo=startupinfo
)
if result.returncode == 0 and result.stdout.strip():
claude_path = result.stdout.strip()
# Clean up any extra output from 'type' command
if " is " in claude_path:
claude_path = claude_path.split(" is ")[-1].strip()
logger.info(f"Found Claude at: {claude_path}")
print(f"Found Claude at: {claude_path}")
return claude_path
except Exception as e:
logger.error(f"Error finding Claude path: {e}")
print(f"Error finding Claude path: {e}")
# Fallback paths to try
fallback_paths = [
"claude", # Try just 'claude' - let WSL PATH handle it
"/usr/local/bin/claude",
"/usr/bin/claude",
"~/.local/bin/claude",
"/home/$USER/.nvm/versions/node/*/bin/claude" # Wildcard for any node version
]
# Test each fallback path
for path in fallback_paths:
try:
# Hide console window on Windows
startupinfo = None
if hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
["wsl", "bash", "-c", f"test -f {path} && echo 'exists'"],
capture_output=True,
text=True,
timeout=2,
startupinfo=startupinfo
)
if result.stdout.strip() == 'exists':
print(f"Found Claude at fallback: {path}")
return path
except:
continue
# If nothing found, default to 'claude' and hope it's in PATH
print("Claude path not found, using 'claude' (hoping it's in PATH)")
return "claude"
def convert_to_wsl_path(self, windows_path: str) -> str:
"""Convert Windows path to WSL format"""
# Handle different path formats
windows_path = windows_path.replace('/', '\\')
# Extract drive letter and path
if ':' in windows_path:
drive = windows_path[0].lower()
path = windows_path[2:].replace('\\', '/')
return f'/mnt/{drive}{path}'
return windows_path
def launch_claude_wsl(self, project_path: str, project_name: str = "Claude Session") -> Union[subprocess.Popen, None]:
"""Launch Claude in WSL terminal for specified directory"""
try:
wsl_path = self.convert_to_wsl_path(project_path)
logger.info(f"Launching Claude for project: {project_name} at path: {project_path}")
# Check if Windows Terminal is available
if os.path.exists(r"C:\Windows\System32\wt.exe"):
# Use Windows Terminal
cmd = [
"wt.exe",
"-w", "0",
"new-tab",
"--title", f"Claude - {project_name}",
"--suppressApplicationTitle",
"--",
"wsl", "bash", "-l", "-i", "-c",
f"cd '{wsl_path}' && echo 'Working directory: {wsl_path}' && echo '' && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
]
logger.info(f"Executing Windows Terminal command: {' '.join(cmd)}")
process = subprocess.Popen(cmd, shell=False)
logger.info(f"Successfully launched Claude in Windows Terminal for: {project_path}")
print(f"Launched Claude in Windows Terminal for: {project_path}")
return process
else:
# Fallback to cmd window
batch_content = f'''@echo off
title Claude - {project_name}
echo Starting Claude for project: {project_name}
echo Working directory: {project_path}
echo.
wsl bash -l -i -c "cd '{wsl_path}' && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
pause
'''
# Create temporary batch file
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
f.write(batch_content)
batch_path = f.name
# Launch in new cmd window
cmd = ['cmd', '/c', 'start', '', batch_path]
logger.info(f"Executing CMD command: {' '.join(cmd)}")
logger.debug(f"Batch file content:\n{batch_content}")
process = subprocess.Popen(cmd, shell=False)
logger.info(f"Successfully launched Claude in CMD for: {project_path}")
print(f"Launched Claude in CMD for: {project_path}")
return process
except Exception as e:
logger.error(f"Error launching Claude: {str(e)}", extra={"project_path": project_path, "project_name": project_name})
print(f"Error launching Claude: {str(e)}")
return None
def launch_claude_vps(self, server: str, username: str, password: str) -> bool:
"""Launch Claude on VPS via SSH"""
try:
# Create SSH command with password
ssh_command = f'ssh {username}@{server}'
# Create batch file for SSH connection
batch_content = f'''@echo off
title Claude - VPS Server
echo Connecting to VPS Server...
echo Server: {server}
echo Username: {username}
echo.
echo Please enter password when prompted: {password}
echo.
echo After connection, type 'claude' to start Claude
echo.
{ssh_command}
pause
'''
# Create temporary batch file
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
f.write(batch_content)
batch_path = f.name
# Launch in new window
if os.path.exists(r"C:\Windows\System32\wt.exe"):
# Windows Terminal
cmd = [
"wt.exe",
"-w", "0",
"new-tab",
"--title", "Claude - VPS",
"--",
"cmd", "/c", batch_path
]
subprocess.Popen(cmd)
else:
# CMD fallback
subprocess.Popen(['cmd', '/c', 'start', '', batch_path])
print(f"Launched VPS connection to: {server}")
return True
except Exception as e:
print(f"Error launching VPS connection: {str(e)}")
return False
def launch_claude_admin_panel(self, project_name: str = "Admin Panel") -> Union[subprocess.Popen, None]:
"""Launch Claude for Admin Panel with directory change to /opt/v2-Docker"""
try:
# Check if Windows Terminal is available
if os.path.exists(r"C:\Windows\System32\wt.exe"):
# Use Windows Terminal
cmd = [
"wt.exe",
"-w", "0",
"new-tab",
"--title", f"Claude - {project_name}",
"--suppressApplicationTitle",
"--",
"wsl", "bash", "-l", "-i", "-c",
f"cd /opt/v2-Docker && echo 'Working directory: /opt/v2-Docker' && echo '' && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
]
process = subprocess.Popen(cmd, shell=False)
print(f"Launched Claude in Windows Terminal for Admin Panel")
return process
else:
# Fallback to cmd window
batch_content = f'''@echo off
title Claude - {project_name}
echo Starting Claude for: {project_name}
echo Working directory: /opt/v2-Docker
echo.
wsl bash -l -i -c "cd /opt/v2-Docker && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
pause
'''
# Create temporary batch file
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
f.write(batch_content)
batch_path = f.name
# Launch in new cmd window
process = subprocess.Popen(['cmd', '/c', 'start', '', batch_path], shell=False)
print(f"Launched Claude in CMD for Admin Panel")
return process
except Exception as e:
print(f"Error launching Claude for Admin Panel: {str(e)}")
return None
# Test functions
if __name__ == "__main__":
launcher = TerminalLauncher()
# Test WSL path conversion
test_paths = [
"C:\\Users\\hendr\\Desktop\\test",
"D:/Projects/MyApp",
"C:\\Program Files\\test"
]
print("Testing WSL path conversion:")
for path in test_paths:
wsl_path = launcher.convert_to_wsl_path(path)
print(f"{path} -> {wsl_path}")

115
test_activity_connection.py Normale Datei
Datei anzeigen

@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Test-Skript für Activity Server Verbindung
"""
import requests
import socket
import time
from urllib.parse import urlparse
def test_connection(server_url="http://91.99.192.14:3001"):
print(f"Teste Verbindung zu: {server_url}")
print("-" * 50)
# Parse URL
parsed = urlparse(server_url)
host = parsed.hostname
port = parsed.port or 80
# 1. DNS/IP Test
print(f"1. Teste DNS-Auflösung für {host}...")
try:
ip = socket.gethostbyname(host)
print(f" ✓ IP-Adresse: {ip}")
except socket.gaierror as e:
print(f" ✗ DNS-Fehler: {e}")
return
# 2. Port Test
print(f"\n2. Teste TCP-Verbindung zu {host}:{port}...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
result = sock.connect_ex((host, port))
if result == 0:
print(f" ✓ Port {port} ist erreichbar")
else:
print(f" ✗ Port {port} ist nicht erreichbar (Fehlercode: {result})")
print(" Mögliche Ursachen:")
print(" - Firewall blockiert die Verbindung")
print(" - Server läuft nicht auf diesem Port")
print(" - Netzwerk-Routing-Problem")
return
except Exception as e:
print(f" ✗ Verbindungsfehler: {e}")
return
finally:
sock.close()
# 3. HTTP Test
print(f"\n3. Teste HTTP-Verbindung...")
try:
response = requests.get(f"{server_url}/health", timeout=5)
print(f" ✓ HTTP Status: {response.status_code}")
if response.status_code == 200:
print(" ✓ Server antwortet korrekt")
print(f" Response: {response.text[:100]}...")
else:
print(f" ⚠ Server antwortet mit Status {response.status_code}")
except requests.exceptions.ConnectionError as e:
print(f" ✗ Verbindungsfehler: {e}")
print(" Mögliche Ursachen:")
print(" - Server ist nicht erreichbar")
print(" - Firewall blockiert HTTP/HTTPS")
except requests.exceptions.Timeout:
print(" ✗ Timeout - Server antwortet nicht rechtzeitig")
except Exception as e:
print(f" ✗ Unerwarteter Fehler: {e}")
# 4. Socket.IO Test
print(f"\n4. Teste Socket.IO Verbindung...")
try:
# Socket.IO initial handshake
response = requests.get(
f"{server_url}/socket.io/?transport=polling&EIO=4",
timeout=5
)
print(f" Status: {response.status_code}")
if response.status_code == 200:
print(" ✓ Socket.IO Endpoint erreichbar")
else:
print(f" ✗ Socket.IO antwortet mit Status {response.status_code}")
except Exception as e:
print(f" ✗ Socket.IO Fehler: {e}")
# 5. Traceroute simulation (simplified)
print(f"\n5. Netzwerk-Route (vereinfacht)...")
import subprocess
try:
if socket.gethostname().lower().startswith("win"):
cmd = ["tracert", "-h", "10", host]
else:
cmd = ["traceroute", "-m", "10", host]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
print(result.stdout[:500])
except:
print(" Traceroute nicht verfügbar")
if __name__ == "__main__":
# Test mit dem Activity Server
test_connection("http://91.99.192.14:3001")
print("\n" + "=" * 50)
print("DIAGNOSE:")
print("=" * 50)
print("\nWenn Port erreichbar aber HTTP nicht funktioniert:")
print("- Prüfen Sie, ob der Activity Server korrekt läuft")
print("- Prüfen Sie die Server-Logs")
print("- Stellen Sie sicher, dass es ein HTTP-Server ist (nicht HTTPS)")
print("\nWenn Port nicht erreichbar:")
print("- Windows Firewall prüfen")
print("- Router/Netzwerk-Firewall prüfen")
print("- VPN-Verbindung prüfen (falls erforderlich)")
print("- Prüfen ob Sie im richtigen Netzwerk sind")

41
test_git_detection.py Normale Datei
Datei anzeigen

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Test script to check if git repository detection is working"""
import os
import json
from pathlib import Path
# Load projects
with open('data/projects.json', 'r') as f:
data = json.load(f)
projects = data.get('projects', [])
print("Testing Git repository detection:\n")
for project in projects:
if project['id'] not in ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent"]:
project_path = Path(project['path'])
git_path = project_path / ".git"
print(f"Project: {project['name']}")
print(f" Path: {project_path}")
print(f" Git repo exists: {git_path.exists()}")
if git_path.exists():
# Check remote
import subprocess
try:
result = subprocess.run(
["git", "remote", "-v"],
cwd=project_path,
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
print(f" Remotes: {result.stdout.strip()}")
else:
print(f" Error checking remotes: {result.stderr}")
except Exception as e:
print(f" Error: {e}")
print()

213
test_refactoring.py Normale Datei
Datei anzeigen

@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
Test script for refactoring verification
Tests that refactored methods work correctly
"""
import sys
import os
from unittest.mock import MagicMock
# Mock GUI dependencies
sys.modules['customtkinter'] = MagicMock()
sys.modules['tkinter'] = MagicMock()
sys.modules['PIL'] = MagicMock()
sys.modules['PIL.Image'] = MagicMock()
# Import after mocking
from gui.config import refactoring_config
from gui.main_window import MainWindow
def test_ui_helpers():
"""Test UI helpers functionality"""
print("\n=== Testing UI Helpers ===")
# Check current configuration
status = refactoring_config.get_status()
print(f"USE_UI_HELPERS: {status['flags']['USE_UI_HELPERS']}")
# Mock required components
with MagicMock() as mock_project_manager:
mock_project_manager.get_projects.return_value = []
# Test load_and_apply_theme
print("\nTesting load_and_apply_theme...")
try:
# Create a minimal MainWindow instance
# This will test if the refactored method is called
import customtkinter as ctk
ctk.CTk = MagicMock(return_value=MagicMock())
# The method should be called during __init__
# Since we can't fully instantiate MainWindow, test the handler directly
from gui.handlers.ui_helpers import UIHelpersHandler
mock_main_window = MagicMock()
mock_main_window.root = MagicMock()
mock_main_window.main_container = MagicMock()
handler = UIHelpersHandler(mock_main_window)
handler.load_and_apply_theme()
print("✅ load_and_apply_theme executed successfully")
# Test _show_scrollable_info
print("\nTesting _show_scrollable_info...")
handler._show_scrollable_info("Test Title", "Test Content")
print("✅ _show_scrollable_info executed successfully")
# Test create_header
print("\nTesting create_header...")
handler.create_header()
print("✅ create_header executed successfully")
# Test create_status_bar
print("\nTesting create_status_bar...")
handler.create_status_bar()
print("✅ create_status_bar executed successfully")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
def test_project_handler():
"""Test project handler functionality"""
print("\n=== Testing Project Handler ===")
from gui.handlers.project_manager import ProjectManagerHandler
from project_manager import Project
mock_main_window = MagicMock()
mock_main_window.project_manager = MagicMock()
mock_main_window.refresh_projects = MagicMock()
mock_main_window.update_status = MagicMock()
handler = ProjectManagerHandler(mock_main_window)
# Test delete_project
print("\nTesting delete_project...")
try:
mock_project = MagicMock()
mock_project.name = "Test Project"
mock_project.id = "test-id"
# Mock the dialog to return False (no delete)
with MagicMock() as mock_messagebox:
mock_messagebox.askyesno.return_value = False
handler.delete_project(mock_project)
print("✅ delete_project executed (no delete)")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
def test_process_handler():
"""Test process handler functionality"""
print("\n=== Testing Process Handler ===")
from gui.handlers.process_manager import ProcessManagerHandler
mock_main_window = MagicMock()
mock_main_window.status_label = MagicMock()
handler = ProcessManagerHandler(mock_main_window)
# Test update_status
print("\nTesting update_status...")
try:
handler.update_status("Test message", error=False)
print("✅ update_status executed successfully")
handler.update_status("Error message", error=True)
print("✅ update_status with error executed successfully")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
# Note: download_log requires file dialog interaction, harder to test
def test_gitea_handler():
"""Test Gitea handler functionality"""
print("\n=== Testing Gitea Handler ===")
from gui.handlers.gitea_operations import GiteaOperationsHandler
mock_main_window = MagicMock()
mock_main_window._show_scrollable_info = MagicMock()
mock_repo_manager = MagicMock()
mock_repo_manager.client = MagicMock()
mock_repo_manager.git_ops = MagicMock()
mock_main_window.repo_manager = mock_repo_manager
handler = GiteaOperationsHandler(mock_main_window)
# Test consolidated test_gitea_connection
print("\nTesting test_gitea_connection (consolidated)...")
try:
# Mock the API calls
mock_repo_manager.client.get_user_info.return_value = {'username': 'testuser'}
mock_repo_manager.client.get_user_orgs.return_value = []
mock_repo_manager.client.get_user_teams.return_value = []
mock_repo_manager.client.get_user_repos.return_value = []
handler.test_gitea_connection()
print("✅ test_gitea_connection executed successfully (without project)")
# Test with project
mock_project = MagicMock()
mock_project.name = "Test Project"
mock_project.path = "/test/path"
handler.test_gitea_connection(mock_project)
print("✅ test_gitea_connection executed successfully (with project)")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
def main():
"""Run all tests"""
print("=== Refactoring Test Suite ===")
# Show current configuration
print("\nCurrent Configuration:")
status = refactoring_config.get_status()
for flag, value in status['flags'].items():
if flag.startswith('USE_'):
status_str = "✅ ENABLED" if value else "❌ DISABLED"
print(f" {flag}: {status_str}")
# Run tests based on enabled features
if status['flags']['USE_UI_HELPERS']:
test_ui_helpers()
else:
print("\n⚠️ UI Helpers not enabled. Enable with:")
print(" python3 manage_refactoring.py enable ui")
if status['flags']['USE_PROJECT_HANDLER']:
test_project_handler()
else:
print("\n⚠️ Project Handler not enabled. Enable with:")
print(" python3 manage_refactoring.py enable project")
if status['flags']['USE_PROCESS_HANDLER']:
test_process_handler()
else:
print("\n⚠️ Process Handler not enabled. Enable with:")
print(" python3 manage_refactoring.py enable process")
if status['flags']['USE_GITEA_HANDLER']:
test_gitea_handler()
else:
print("\n⚠️ Gitea Handler not enabled. Enable with:")
print(" python3 manage_refactoring.py enable gitea")
print("\n✅ Test completed")
if __name__ == '__main__':
main()

150
tests/test_main_window_api.py Normale Datei
Datei anzeigen

@ -0,0 +1,150 @@
"""
API Compatibility Tests for MainWindow Refactoring
Dokumentiert alle öffentlichen Methoden der MainWindow Klasse
um sicherzustellen, dass keine Breaking Changes auftreten.
"""
import unittest
from unittest.mock import Mock, patch, MagicMock
import sys
import os
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Mock all GUI dependencies before import
sys.modules['customtkinter'] = MagicMock()
sys.modules['tkinter'] = MagicMock()
sys.modules['PIL'] = MagicMock()
sys.modules['PIL.Image'] = MagicMock()
class TestMainWindowAPI(unittest.TestCase):
"""Test to ensure all public methods exist after refactoring"""
# Liste aller öffentlichen Methoden der MainWindow Klasse
# Generiert aus der Analyse vom 2025-07-05
REQUIRED_PUBLIC_METHODS = [
'__init__',
'setup_ui',
'create_header',
'create_content_area',
'create_status_bar',
'refresh_projects',
'create_project_tile',
'create_add_tile',
'create_project_tile_flow',
'create_add_tile_flow',
'add_new_project',
'open_project',
'open_vps_connection',
'open_admin_panel',
'open_vps_docker',
'open_readme',
'delete_project',
'rename_project',
'generate_readme_background',
'update_status',
'download_log',
'refresh_ui',
'load_and_apply_theme',
'on_window_resize',
'stop_project',
'setup_interaction_tracking',
'monitor_process',
'check_process_status',
'open_gitea_window',
'on_gitea_repo_select',
'clear_project_selection',
'on_project_select',
'show_git_status',
'commit_changes',
'push_to_gitea',
'pull_from_gitea',
'fetch_from_gitea',
'manage_branches',
'link_to_gitea',
'clone_repository',
'create_project_from_repo',
'on_closing',
'gitea_operation',
'init_and_push_to_gitea',
'init_git_repo',
'test_gitea_connection',
'verify_repository_on_gitea',
'fix_repository_issues',
'manage_large_files',
'setup_git_lfs',
'run'
]
# Private/Protected Methoden die NICHT Teil der öffentlichen API sind
PRIVATE_METHODS = [
'_differential_update',
'_update_project_tiles_colors',
'_on_click_start',
'_on_click_end',
'_on_dropdown_select',
'_on_focus_in',
'_on_focus_out',
'_check_pending_updates',
'_handle_process_ended',
'_create_project_after_clone',
'_show_scrollable_info'
]
@patch('gui.main_window.logger')
@patch('gui.main_window.ProjectProcessTracker')
@patch('gui.main_window.ProcessManager')
@patch('gui.main_window.VPSConnection')
@patch('gui.main_window.ReadmeGenerator')
@patch('gui.main_window.TerminalLauncher')
@patch('gui.main_window.ProjectManager')
@patch('gui.main_window.RepositoryManager')
def test_all_public_methods_exist(self, *mocks):
"""Testet dass alle öffentlichen Methoden existieren"""
# Import after all mocks are set up
from gui.main_window import MainWindow
# Check that all required methods exist
missing_methods = []
for method_name in self.REQUIRED_PUBLIC_METHODS:
if not hasattr(MainWindow, method_name):
missing_methods.append(method_name)
self.assertEqual(
missing_methods,
[],
f"Following public methods are missing: {missing_methods}"
)
def test_no_unexpected_public_methods(self):
"""Warnt vor neuen öffentlichen Methoden die dokumentiert werden sollten"""
# This test will be implemented after import works
pass
class TestMainWindowBehavior(unittest.TestCase):
"""Behavioral tests for critical MainWindow operations"""
def setUp(self):
"""Set up test fixtures"""
self.window = None
def test_project_lifecycle(self):
"""Test basic project creation, opening and deletion"""
# TODO: Implement after mocking is complete
pass
def test_gitea_operations(self):
"""Test Gitea integration operations"""
# TODO: Test init_and_push_to_gitea, push_to_gitea, etc.
pass
def test_duplicate_methods_behavior(self):
"""Ensure duplicate methods have consistent behavior"""
# TODO: Compare behavior of duplicate methods
pass
if __name__ == '__main__':
unittest.main()

23
tools/download_winscp.txt Normale Datei
Datei anzeigen

@ -0,0 +1,23 @@
WinSCP Portable Download Instructions
=====================================
1. Download WinSCP Portable from:
https://winscp.net/download/WinSCP-6.3.6-Portable.zip
Alternative direct link:
https://sourceforge.net/projects/winscp/files/WinSCP/6.3.6/WinSCP-6.3.6-Portable.zip/download
2. Extract the ZIP file to this folder:
tools/WinSCP/
3. The structure should be:
tools/
└── WinSCP/
├── WinSCP.exe
├── WinSCP.com
└── (other files)
4. After extraction, the WinSCP button in Claude Project Manager will work automatically.
Note: WinSCP Portable is about 10 MB and doesn't require installation.
It's a trusted SFTP/FTP client for Windows.

210
utils/logger.py Normale Datei
Datei anzeigen

@ -0,0 +1,210 @@
import logging
import datetime
import os
from pathlib import Path
from typing import Optional, Any
import json
from threading import Lock
import traceback
import inspect
class ApplicationLogger:
_instance = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, 'initialized'):
self.initialized = True
self.log_entries = []
self.max_entries = 50000 # Increased for more detailed logging
self.interaction_count = 0
# Create logs directory if it doesn't exist
self.log_dir = Path("logs")
self.log_dir.mkdir(exist_ok=True)
# Setup file logging
self.log_file = self.log_dir / f"cpm_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
# Configure logger
self.logger = logging.getLogger('CPM')
self.logger.setLevel(logging.DEBUG)
# File handler
file_handler = logging.FileHandler(self.log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Detailed Formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S.%f'[:-3] # Include milliseconds
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Add handlers
self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)
# Custom handler to store in memory
self.memory_handler = MemoryHandler(self)
self.memory_handler.setFormatter(formatter)
self.logger.addHandler(self.memory_handler)
self.info("Claude Project Manager - Logger initialized")
def debug(self, message: str, extra: Optional[dict] = None):
self._log('DEBUG', message, extra)
def info(self, message: str, extra: Optional[dict] = None):
self._log('INFO', message, extra)
def warning(self, message: str, extra: Optional[dict] = None):
self._log('WARNING', message, extra)
def error(self, message: str, extra: Optional[dict] = None, exc_info=None):
self._log('ERROR', message, extra, exc_info)
def critical(self, message: str, extra: Optional[dict] = None, exc_info=None):
self._log('CRITICAL', message, extra, exc_info)
def _log(self, level: str, message: str, extra: Optional[dict] = None, exc_info=None):
# Get caller information
frame = inspect.currentframe()
if frame and frame.f_back and frame.f_back.f_back:
caller = frame.f_back.f_back
caller_info = f"{caller.f_code.co_filename}:{caller.f_lineno}"
else:
caller_info = "unknown"
log_method = getattr(self.logger, level.lower())
if extra:
message = f"{message} | Extra: {json.dumps(extra, default=str)}"
if exc_info:
log_method(message, exc_info=exc_info)
else:
log_method(message)
def add_entry(self, timestamp: str, level: str, message: str):
"""Add log entry to memory buffer"""
with self._lock:
self.log_entries.append({
'timestamp': timestamp,
'level': level,
'message': message
})
# Keep only the latest entries
if len(self.log_entries) > self.max_entries:
self.log_entries = self.log_entries[-self.max_entries:]
def get_log_content(self) -> str:
"""Get all log entries as formatted string"""
with self._lock:
lines = []
for entry in self.log_entries:
lines.append(f"{entry['timestamp']} - {entry['level']} - {entry['message']}")
return '\n'.join(lines)
def export_logs(self, filepath: str, include_system_info: bool = True):
"""Export logs to a file with optional system information"""
content = []
if include_system_info:
content.append("=" * 80)
content.append("CLAUDE PROJECT MANAGER - LOG EXPORT")
content.append("=" * 80)
content.append(f"Export Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
content.append(f"Log File: {self.log_file}")
content.append(f"Total Interactions: {self.interaction_count}")
content.append(f"Total Log Entries: {len(self.log_entries)}")
content.append("=" * 80)
content.append("")
content.append(self.get_log_content())
with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(content) if isinstance(content, list) else content)
def clear_logs(self):
"""Clear in-memory log entries"""
with self._lock:
self.log_entries.clear()
self.info("Log entries cleared")
class MemoryHandler(logging.Handler):
"""Custom handler to store logs in memory"""
def __init__(self, logger_instance):
super().__init__()
self.logger_instance = logger_instance
def emit(self, record):
try:
msg = self.format(record)
# Extract timestamp, level and message
parts = msg.split(' - ', 3)
if len(parts) >= 4:
timestamp = parts[0]
level = parts[2]
message = parts[3]
self.logger_instance.add_entry(timestamp, level, message)
except Exception:
self.handleError(record)
def log_interaction(self, component: str, action: str, details: Optional[dict] = None):
"""Log UI interactions"""
self.interaction_count += 1
interaction_msg = f"UI_INTERACTION #{self.interaction_count} - Component: {component}, Action: {action}"
if details:
interaction_msg += f", Details: {json.dumps(details, default=str)}"
self.info(interaction_msg)
def log_git_operation(self, operation: str, status: str, details: Optional[dict] = None):
"""Log Git operations with details"""
git_msg = f"GIT_OPERATION - Operation: {operation}, Status: {status}"
if details:
git_msg += f", Details: {json.dumps(details, default=str)}"
self.info(git_msg)
def log_exception(self, exception: Exception, context: str = ""):
"""Log exceptions with full traceback"""
tb = traceback.format_exc()
error_msg = f"EXCEPTION in {context}: {type(exception).__name__}: {str(exception)}\nTraceback:\n{tb}"
self.error(error_msg)
def log_method_call(self, method_name: str, args: tuple = None, kwargs: dict = None):
"""Log method calls with arguments"""
call_msg = f"METHOD_CALL - {method_name}"
if args:
call_msg += f", Args: {args}"
if kwargs:
call_msg += f", Kwargs: {kwargs}"
self.debug(call_msg)
def log_state_change(self, component: str, old_state: Any, new_state: Any):
"""Log state changes in the application"""
state_msg = f"STATE_CHANGE - Component: {component}, Old: {old_state}, New: {new_state}"
self.info(state_msg)
def get_full_log_path(self) -> str:
"""Get the full path to the current log file"""
return str(self.log_file.absolute())
def get_all_log_files(self) -> list:
"""Get all log files in the log directory"""
return sorted([f for f in self.log_dir.glob('*.log')], reverse=True)
# Global logger instance
logger = ApplicationLogger()

425
vps_connection.py Normale Datei
Datei anzeigen

@ -0,0 +1,425 @@
"""
VPS Connection Module
Handles SSH connection to VPS server
"""
import os
import tempfile
import subprocess
from utils.logger import logger
class VPSConnection:
def __init__(self):
self.server = "91.99.192.14"
self.username = "claude-dev"
self.password = "z0E1Al}q2H?Yqd!O"
logger.info("VPSConnection initialized")
def create_ssh_script(self) -> str:
"""Create a script for SSH connection with automatic password and Claude start"""
import tempfile
import os
logger.info(f"Creating SSH script for VPS connection to {self.server}")
# Create a simple bash script for SSH connection
bash_script = f'''#!/bin/bash
echo "================================================================================"
echo " Claude VPS Server Connection"
echo "================================================================================"
echo ""
echo "Server: {self.server}"
echo "Username: {self.username}"
echo ""
# Try to use sshpass if available
if command -v sshpass >/dev/null 2>&1; then
echo "Connecting to VPS server and starting Claude automatically..."
echo ""
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "claude || bash"
else
# Try to install sshpass
echo "Installing sshpass for automatic login..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq 2>/dev/null
sudo apt-get install -y sshpass -qq 2>/dev/null
fi
# Check again if sshpass is now available
if command -v sshpass >/dev/null 2>&1; then
echo "sshpass installed successfully!"
echo "Connecting to VPS server and starting Claude automatically..."
echo ""
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "claude || bash"
else
echo "Manual password entry required."
echo ""
echo "Password: {self.password}"
echo "(Right-click to paste)"
echo ""
echo "After login, type: claude"
echo ""
ssh -o StrictHostKeyChecking=no {self.username}@{self.server}
fi
fi
echo ""
echo "Connection closed."
echo "Press Enter to exit..."
read
'''
# Save bash script with Unix line endings
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, newline='\n') as f:
f.write(bash_script)
bash_path = f.name
# Convert to WSL path
bash_wsl_path = bash_path.replace('\\', '/').replace('C:', '/mnt/c')
script_content = f"""@echo off
cls
REM Convert line endings and launch WSL with the bash script
wsl dos2unix {bash_wsl_path} 2>nul || wsl sed -i 's/\r$//' {bash_wsl_path}
wsl chmod +x {bash_wsl_path}
wsl bash {bash_wsl_path}
REM Clean up temp file
del "{bash_path}" 2>nul
"""
return script_content
def create_readme_content(self) -> str:
"""Generate README content for VPS project"""
from datetime import datetime
content = f"""# Claude VPS Server
*This README was automatically generated by Claude Project Manager*
## Server Information
- **Server IP**: {self.server}
- **Username**: {self.username}
- **Connection**: SSH (Port 22)
## How to Connect
### Using Claude Project Manager
1. Click on the VPS Server tile
2. Terminal will open with connection instructions
3. Enter the password when prompted
4. Type `claude` after successful login
### Manual Connection
```bash
ssh {self.username}@{self.server}
```
## Available Commands
After connecting to the VPS:
- `claude` - Start Claude CLI
- `ls` - List files
- `cd <directory>` - Change directory
- `exit` - Close SSH connection
## Security Notes
- Keep your password secure
- Don't share SSH credentials
- Always logout when finished (`exit` command)
## Features
This VPS server provides:
- Remote access to Claude
- Persistent environment
- Isolated workspace
- Full Linux environment
## Troubleshooting
### Connection Issues
1. Check internet connection
2. Verify server is online
3. Ensure SSH port (22) is not blocked
4. Try manual SSH command
### Authentication Failed
- Verify password is correct
- Check username spelling
- Ensure caps lock is off
---
## Connection Log
- README generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
"""
return content
def generate_vps_readme(self, readme_path: str):
"""Generate and save VPS README"""
logger.info(f"Generating VPS README at: {readme_path}")
content = self.create_readme_content()
# Create directory if needed
os.makedirs(os.path.dirname(readme_path), exist_ok=True)
# Write README
with open(readme_path, 'w', encoding='utf-8') as f:
f.write(content)
return readme_path
def create_admin_panel_script(self) -> str:
"""Create a script for SSH connection to Admin Panel with directory change"""
import tempfile
import os
logger.info("Creating Admin Panel SSH script")
# Create a simple bash script for SSH connection
bash_script = f'''#!/bin/bash
echo "================================================================================"
echo " Claude Admin Panel Connection"
echo "================================================================================"
echo ""
echo "Server: {self.server}"
echo "Username: {self.username}"
echo "Target Directory: /opt/v2-Docker"
echo ""
# Try to use sshpass if available
if command -v sshpass >/dev/null 2>&1; then
echo "Connecting to VPS server and starting Claude in Admin Panel..."
echo ""
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker && echo 'Changed to /opt/v2-Docker' && claude || bash"
else
# Try to install sshpass
echo "Installing sshpass for automatic login..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq 2>/dev/null
sudo apt-get install -y sshpass -qq 2>/dev/null
fi
# Check again if sshpass is now available
if command -v sshpass >/dev/null 2>&1; then
echo "sshpass installed successfully!"
echo "Connecting to VPS server and starting Claude in Admin Panel..."
echo ""
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker && echo 'Changed to /opt/v2-Docker' && claude || bash"
else
echo "Manual password entry required."
echo ""
echo "Password: {self.password}"
echo "(Right-click to paste)"
echo ""
echo "After login:"
echo "1. Type: cd /opt/v2-Docker"
echo "2. Type: claude"
echo ""
ssh -o StrictHostKeyChecking=no {self.username}@{self.server}
fi
fi
echo ""
echo "Connection closed."
echo "Press Enter to exit..."
read
'''
# Save bash script with Unix line endings
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, newline='\n') as f:
f.write(bash_script)
bash_path = f.name
# Convert to WSL path
bash_wsl_path = bash_path.replace('\\', '/').replace('C:', '/mnt/c')
script_content = f"""@echo off
cls
REM Convert line endings and launch WSL with the bash script
wsl dos2unix {bash_wsl_path} 2>nul || wsl sed -i 's/\r$//' {bash_wsl_path}
wsl chmod +x {bash_wsl_path}
wsl bash {bash_wsl_path}
REM Clean up temp file
del "{bash_path}" 2>nul
"""
return script_content
def create_vps_docker_script(self) -> str:
"""Create a script for SSH connection to VPS Docker with docker compose commands"""
import tempfile
import os
logger.info("Creating VPS Docker restart script")
# Create a bash script for SSH connection with docker commands
bash_script = f'''#!/bin/bash
echo "================================================================================"
echo " VPS Docker Admin Panel Restart"
echo "================================================================================"
echo ""
echo "Server: {self.server}"
echo "Username: {self.username}"
echo "Target Directory: /opt/v2-Docker/v2"
echo ""
# Try to use sshpass if available
if command -v sshpass >/dev/null 2>&1; then
echo "Connecting to VPS server and restarting Admin Panel..."
echo ""
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker/v2 && echo -e '\\n[Restarting Admin Panel Docker containers...]\\n' && docker compose down && echo -e '\\n[Building containers with no cache...]\\n' && docker compose build --no-cache && echo -e '\\n[Starting containers in detached mode...]\\n' && docker compose up -d && echo -e '\\n[SUCCESS] Admin Panel restart completed successfully!\\n' || echo -e '\\n[ERROR] Error during restart process!\\n'"
else
# Try to install sshpass
echo "Installing sshpass for automatic login..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq 2>/dev/null
sudo apt-get install -y sshpass -qq 2>/dev/null
fi
# Check again if sshpass is now available
if command -v sshpass >/dev/null 2>&1; then
echo "sshpass installed successfully!"
echo "Connecting to VPS server and restarting Admin Panel..."
echo ""
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker/v2 && echo -e '\\n[Restarting Admin Panel Docker containers...]\\n' && docker compose down && echo -e '\\n[Building containers with no cache...]\\n' && docker compose build --no-cache && echo -e '\\n[Starting containers in detached mode...]\\n' && docker compose up -d && echo -e '\\n[SUCCESS] Admin Panel restart completed successfully!\\n' || echo -e '\\n[ERROR] Error during restart process!\\n'"
else
echo "Manual password entry required."
echo ""
echo "Password: {self.password}"
echo "(Right-click to paste)"
echo ""
echo "After login, the following commands will be executed:"
echo "1. cd /opt/v2-Docker/v2"
echo "2. docker compose down"
echo "3. docker compose build --no-cache"
echo "4. docker compose up -d"
echo ""
ssh -o StrictHostKeyChecking=no {self.username}@{self.server}
fi
fi
echo ""
echo "Press Enter to close..."
read
'''
# Save bash script with Unix line endings
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, newline='\n') as f:
f.write(bash_script)
bash_path = f.name
# Convert to WSL path
bash_wsl_path = bash_path.replace('\\', '/').replace('C:', '/mnt/c')
script_content = f"""@echo off
cls
REM Convert line endings and launch WSL with the bash script
wsl dos2unix {bash_wsl_path} 2>nul || wsl sed -i 's/\r$//' {bash_wsl_path}
wsl chmod +x {bash_wsl_path}
wsl bash {bash_wsl_path}
REM Clean up temp file
del "{bash_path}" 2>nul
"""
return script_content
def create_activity_server_script(self) -> str:
"""Create a script for SSH connection to Activity Server"""
import tempfile
import os
logger.info("Creating Activity Server connection script")
# Create a robust script with multiple approaches
script_content = f"""@echo off
cls
echo ================================================================================
echo CPM Activity Server Connection
echo ================================================================================
echo.
echo Server: {self.server}
echo Username: {self.username}
echo Target Directory: /home/claude-dev/cpm-activity-server
echo.
REM Check if PowerShell is available and use it for better plink integration
where powershell >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo Using PowerShell for enhanced connection...
powershell -NoProfile -ExecutionPolicy Bypass -Command "& {{$plinkPath = Get-Command plink -ErrorAction SilentlyContinue; if ($plinkPath) {{ Write-Host 'Connecting via plink...' -ForegroundColor Green; Write-Host ''; $process = Start-Process -FilePath 'plink' -ArgumentList '-ssh', '-l', '{self.username}', '-pw', '{self.password}', '-t', '{self.server}', 'cd /home/claude-dev/cpm-activity-server && echo \"Successfully changed to Activity Server directory\" && echo && bash -l' -PassThru -NoNewWindow -Wait; if ($process.ExitCode -ne 0) {{ Write-Host ''; Write-Host 'Automatic directory change failed. Starting interactive session...' -ForegroundColor Yellow; Write-Host ''; Write-Host 'IMPORTANT: After login, run these commands:' -ForegroundColor Red; Write-Host ' cd /home/claude-dev/cpm-activity-server' -ForegroundColor White; Write-Host ' claude' -ForegroundColor White; Write-Host ''; & plink -ssh -l {self.username} -pw '{self.password}' -t {self.server} }} }} else {{ Write-Host 'Plink not found, trying standard SSH...' -ForegroundColor Yellow; Write-Host 'Password: {self.password}'; Write-Host 'After login, run: cd /home/claude-dev/cpm-activity-server && claude'; & ssh {self.username}@{self.server} }} }}"
goto end
)
REM First, try the plink approach with RemoteCommand
where plink >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo Using PuTTY plink for connection...
echo.
REM Try plink with direct command execution
echo Attempting automatic directory change...
plink -batch -ssh -l {self.username} -pw "{self.password}" {self.server} "cd /home/claude-dev/cpm-activity-server && exec bash -l"
REM If that didn't work, try interactive mode
if %ERRORLEVEL% NEQ 0 (
echo.
echo Direct command execution failed. Starting interactive session...
echo.
echo ================================================================================
echo IMPORTANT: After login, please run these commands:
echo.
echo cd /home/claude-dev/cpm-activity-server
echo claude
echo.
echo ================================================================================
echo.
plink -ssh -l {self.username} -pw "{self.password}" -t {self.server}
)
goto end
)
REM Check if we have WSL available
where wsl >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo Using WSL for connection...
echo.
REM Create inline bash command for WSL
wsl bash -c "echo 'Connecting to Activity Server...' && if command -v sshpass >/dev/null 2>&1; then sshpass -p '{self.password}' ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} 'cd /home/claude-dev/cpm-activity-server && echo \"Successfully changed to Activity Server directory\" && echo && claude || bash -l'; else echo 'Manual password entry required.' && echo 'Password: {self.password}' && echo && echo 'After login, run: cd /home/claude-dev/cpm-activity-server && claude' && echo && ssh -o StrictHostKeyChecking=no {self.username}@{self.server}; fi"
goto end
)
REM Fallback to standard SSH
echo No automated tools found. Using standard SSH...
echo.
echo ================================================================================
echo Connection Instructions:
echo.
echo 1. Enter this password when prompted: {self.password}
echo (You can right-click to paste in most terminals)
echo.
echo 2. After successful login, run these commands:
echo cd /home/claude-dev/cpm-activity-server
echo claude
echo.
echo ================================================================================
echo.
pause
ssh {self.username}@{self.server}
:end
echo.
echo Connection closed.
pause
"""
return script_content
# Test module
if __name__ == "__main__":
vps = VPSConnection()
print("VPS Connection Module")
print(f"Server: {vps.server}")
print(f"Username: {vps.username}")
print("\nSSH Script Preview:")
print(vps.create_ssh_script()[:300] + "...")