diff --git a/gitpusher/.gitignore b/gitpusher/.gitignore
new file mode 100644
index 0000000..4928eb5
--- /dev/null
+++ b/gitpusher/.gitignore
@@ -0,0 +1,24 @@
+# Data files (contains secrets and logs)
+data/*.json
+
+# Log files
+*.log
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Temporary files
+*.tmp
+*.temp
+
+# Backup files
+*.backup
+*.bak
diff --git a/gitpusher/.htaccess b/gitpusher/.htaccess
new file mode 100644
index 0000000..d71e802
--- /dev/null
+++ b/gitpusher/.htaccess
@@ -0,0 +1,14 @@
+# Deny access to data directory from web
+
+ Require all denied
+
+
+# Deny access to sensitive files
+
+ Require all denied
+
+
+# Protect .git directory if exists
+
+ Require all denied
+
diff --git a/gitpusher/INSTALL.md b/gitpusher/INSTALL.md
new file mode 100644
index 0000000..ddea833
--- /dev/null
+++ b/gitpusher/INSTALL.md
@@ -0,0 +1,284 @@
+# Schnellstart-Installation
+
+Schritt-für-Schritt Anleitung zur Installation von GitHub Sync auf deinem Ubuntu LXC Container.
+
+## ⚡ Express-Installation (5 Minuten)
+
+### 1. System vorbereiten
+
+```bash
+# System aktualisieren
+sudo apt update && sudo apt upgrade -y
+
+# Benötigte Pakete installieren
+sudo apt install -y apache2 php libapache2-mod-php php-cli php-json php-mbstring git
+```
+
+### 2. Apache Module aktivieren
+
+```bash
+sudo a2enmod rewrite
+sudo systemctl restart apache2
+```
+
+### 3. Virtual Host konfigurieren
+
+```bash
+# Virtual Host Datei erstellen
+sudo nano /etc/apache2/sites-available/github-sync.conf
+```
+
+Kopiere diese Konfiguration:
+
+```apache
+
+ ServerName github-sync.local
+ DocumentRoot /gitpusher/public
+
+
+ Options -Indexes +FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+
+ Require all denied
+
+
+
+ Require all denied
+
+
+ ErrorLog ${APACHE_LOG_DIR}/github-sync-error.log
+ CustomLog ${APACHE_LOG_DIR}/github-sync-access.log combined
+
+```
+
+```bash
+# Site aktivieren
+sudo a2ensite github-sync.conf
+sudo a2dissite 000-default.conf # Optional: Default-Site deaktivieren
+sudo systemctl reload apache2
+```
+
+### 4. Berechtigungen setzen
+
+```bash
+# Eigentümer ändern
+sudo chown -R www-data:www-data /gitpusher
+
+# Berechtigungen setzen
+sudo chmod 755 /gitpusher
+sudo chmod 755 /gitpusher/public
+sudo chmod 755 /gitpusher/data
+sudo chmod 755 /gitpusher/src
+sudo chmod 600 /gitpusher/data/*.json
+```
+
+### 5. GitHub Personal Access Token erstellen
+
+1. Gehe zu: https://github.com/settings/tokens
+2. Klicke: **"Generate new token (classic)"**
+3. Name: `GitHub Sync Server`
+4. Scope: ✅ **repo** (Full control of private repositories)
+5. Klicke: **"Generate token"**
+6. **Kopiere den Token** (ghp_...)
+
+### 6. Token hinterlegen
+
+```bash
+sudo nano /gitpusher/data/secrets.json
+```
+
+Ersetze die leere Zeile:
+
+```json
+{
+ "github_pat": "ghp_DEIN_TOKEN_HIER",
+ "webhook_secrets": {}
+}
+```
+
+Speichern: `Ctrl+O` → `Enter` → `Ctrl+X`
+
+### 7. Testen
+
+```bash
+# Apache Status prüfen
+sudo systemctl status apache2
+
+# PHP testen
+php -v
+
+# Git testen
+git --version
+```
+
+### 8. Im Browser öffnen
+
+Öffne in deinem Browser:
+- Wenn du eine Domain hast: `http://github-sync.deine-domain.de`
+- Sonst mit IP: `http://DEINE-SERVER-IP`
+
+**Du solltest jetzt das Dashboard sehen!** 🎉
+
+## 🎯 Erstes Repository hinzufügen
+
+1. Klicke im Dashboard auf **"+ Repository hinzufügen"**
+2. Fülle aus:
+ ```
+ Name: Test-Projekt
+ Repository URL: https://github.com/dein-username/dein-repo.git
+ Branch: main
+ Ziel-Pfad: /var/www/test-projekt
+ Auto-Sync: ✅ Aktiviert
+ ```
+3. Klicke **"Repository hinzufügen"**
+
+Das Repository wird automatisch geklont!
+
+## 🔗 GitHub Webhook einrichten
+
+Nach dem Hinzufügen siehst du ein Modal mit Webhook-Informationen.
+
+1. Kopiere **Payload URL** und **Secret**
+2. Gehe zu deinem GitHub Repo → **Settings** → **Webhooks** → **Add webhook**
+3. Füge ein:
+ - **Payload URL**: (kopiert)
+ - **Content type**: `application/json`
+ - **Secret**: (kopiert)
+ - **Events**: "Just the push event"
+4. Klicke **"Add webhook"**
+
+Fertig! Bei jedem Push wird automatisch synchronisiert.
+
+## ✅ Erfolgs-Check
+
+Teste die Synchronisation:
+
+1. Ändere eine Datei in deinem GitHub-Repo
+2. Committe und pushe die Änderung
+3. Schau im Dashboard → Log-Einträge
+4. Du solltest sehen: "✅ Sync OK (X Dateien)"
+
+Prüfe die Datei auf dem Server:
+```bash
+ls -la /var/www/test-projekt
+```
+
+## 🔧 Erweiterte Konfiguration
+
+### SSL/HTTPS einrichten (empfohlen für Produktion)
+
+```bash
+# Let's Encrypt installieren
+sudo apt install certbot python3-certbot-apache
+
+# Zertifikat erstellen
+sudo certbot --apache -d github-sync.deine-domain.de
+
+# Auto-Renewal testen
+sudo certbot renew --dry-run
+```
+
+### Firewall konfigurieren
+
+```bash
+# UFW Firewall aktivieren
+sudo ufw allow 'Apache Full'
+sudo ufw enable
+```
+
+### Log-Rotation einrichten
+
+```bash
+sudo nano /etc/logrotate.d/github-sync
+```
+
+```
+/var/log/apache2/github-sync-*.log {
+ daily
+ missingok
+ rotate 14
+ compress
+ delaycompress
+ notifempty
+ create 0640 root adm
+ sharedscripts
+ postrotate
+ systemctl reload apache2 > /dev/null
+ endscript
+}
+```
+
+## 🚨 Häufige Probleme
+
+### Problem: "403 Forbidden" beim Öffnen
+
+**Lösung:**
+```bash
+sudo chown -R www-data:www-data /gitpusher
+sudo chmod 755 /gitpusher/public
+```
+
+### Problem: Repository kann nicht geklont werden
+
+**Lösung:**
+```bash
+# Prüfe, ob www-data git nutzen kann
+sudo -u www-data git --version
+
+# Prüfe, ob Ziel-Ordner Schreibrechte hat
+sudo -u www-data mkdir -p /var/www/test
+```
+
+### Problem: Webhook kommt nicht an
+
+**Lösung:**
+1. Prüfe GitHub Webhook Deliveries auf Fehler
+2. Prüfe Firewall: `sudo ufw status`
+3. Prüfe Apache Logs:
+ ```bash
+ sudo tail -f /var/log/apache2/github-sync-error.log
+ ```
+
+### Problem: JSON-Dateien leer oder defekt
+
+**Lösung:**
+```bash
+# Setze Standardwerte zurück
+cd /gitpusher/data
+
+echo '{"repositories":[]}' | sudo tee config.json
+echo '{"entries":[]}' | sudo tee log.json
+echo '{"github_pat":"","webhook_secrets":{}}' | sudo tee secrets.json
+
+sudo chmod 600 *.json
+sudo chown www-data:www-data *.json
+```
+
+## 📋 Checkliste
+
+- [ ] Apache installiert und läuft
+- [ ] PHP installiert (Version 7.4+)
+- [ ] Git installiert
+- [ ] Virtual Host konfiguriert
+- [ ] Site aktiviert und Apache neu geladen
+- [ ] Berechtigungen gesetzt (www-data)
+- [ ] GitHub PAT erstellt und hinterlegt
+- [ ] Dashboard im Browser erreichbar
+- [ ] Erstes Repository hinzugefügt
+- [ ] Webhook in GitHub eingerichtet
+- [ ] Test-Push erfolgreich synchronisiert
+
+## 🎓 Nächste Schritte
+
+1. Lies die vollständige [README.md](README.md)
+2. Füge weitere Repositories hinzu
+3. Teste Rollback-Funktion
+4. Richte SSL/HTTPS ein (für Produktion)
+5. Konfiguriere Monitoring
+
+---
+
+**Viel Erfolg!** 🚀
diff --git a/gitpusher/PROJECT_STRUCTURE.md b/gitpusher/PROJECT_STRUCTURE.md
new file mode 100644
index 0000000..e21f8cb
--- /dev/null
+++ b/gitpusher/PROJECT_STRUCTURE.md
@@ -0,0 +1,447 @@
+# Projektstruktur
+
+## Übersicht
+
+```
+/gitpusher/
+│
+├── 📄 README.md # Hauptdokumentation
+├── 📄 INSTALL.md # Schnellstart-Installation
+├── 📄 PROJECT_STRUCTURE.md # Diese Datei
+├── 🔒 .htaccess # Hauptsicherheitskonfiguration
+├── 📝 .gitignore # Git-Ignore-Regeln
+│
+├── 📁 public/ # Web-Root (Apache DocumentRoot)
+│ ├── 🌐 index.php # Dashboard (Frontend)
+│ ├── 🔗 webhook.php # Webhook-Endpoint für GitHub
+│ │
+│ ├── 📁 api/ # REST API Endpunkte
+│ │ ├── repos.php # Repository-CRUD-Operationen
+│ │ ├── sync.php # Manueller Sync-Trigger
+│ │ ├── rollback.php # Rollback zu älterem Commit
+│ │ └── log.php # Log-Abfrage
+│ │
+│ ├── 📁 css/
+│ │ └── style.css # Komplettes Dashboard-Styling
+│ │
+│ └── 📁 js/
+│ └── app.js # Frontend-Logik & AJAX
+│
+├── 📁 data/ # Datenspeicher (NICHT web-zugänglich!)
+│ ├── 🔒 .htaccess # Zugriff komplett verweigern
+│ ├── 📊 config.json # Repository-Konfigurationen
+│ ├── 📜 log.json # Alle Log-Einträge
+│ └── 🔐 secrets.json # GitHub PAT & Webhook Secrets
+│
+└── 📁 src/ # PHP Backend-Klassen
+ ├── 🔒 .htaccess # Zugriff komplett verweigern
+ ├── ConfigManager.php # JSON-Dateiverwaltung
+ ├── Logger.php # Logging-System
+ └── GitHandler.php # Git-Operationen (clone, pull, revert)
+```
+
+## 📂 Detaillierte Beschreibung
+
+### `/public/` - Web-Root
+
+**Apache DocumentRoot** - Einziger Ordner, der über HTTP erreichbar ist.
+
+#### `index.php` - Dashboard
+- Haupt-Frontend der Anwendung
+- Zeigt alle Repositories mit Status
+- Statistiken (Anzahl Repos, Sync-Status, etc.)
+- Log-Anzeige der letzten Ereignisse
+- Modals für: Repository hinzufügen, Rollback, Webhook-Info
+
+#### `webhook.php` - GitHub Webhook Endpoint
+- Empfängt POST-Requests von GitHub
+- Verifiziert Webhook-Signatur (HMAC SHA-256)
+- Prüft, ob Push zum konfigurierten Branch gehört
+- Triggert automatischen `git pull`
+- Loggt alle Ereignisse
+
+#### `/api/` - REST API
+
+##### `repos.php`
+- **GET**: Liste aller Repositories oder einzelnes Repository
+- **POST**: Neues Repository hinzufügen (+ initial clone)
+- **PUT**: Repository-Einstellungen aktualisieren
+- **DELETE**: Repository aus Config entfernen (optional: Dateien löschen)
+
+##### `sync.php`
+- **POST**: Manuellen Sync durchführen
+- Führt `git pull` für angegebenes Repository aus
+- Gibt Anzahl geänderter Dateien zurück
+
+##### `rollback.php`
+- **GET**: Liste der letzten Commits (für Rollback-Auswahl)
+- **POST**: Rollback zu bestimmtem Commit durchführen
+- Nutzt `git revert` (sicher, keine Commits gelöscht)
+
+##### `log.php`
+- **GET**: Log-Einträge abrufen
+- Filter: Repository-ID, Log-Type, Limit, Offset
+- Gibt Statistiken zurück (Erfolg/Fehler/Warnung)
+
+#### `/css/style.css`
+- Modernes, responsives Design
+- CSS Custom Properties (CSS Variables)
+- Mobile-First Ansatz
+- Animationen für Toasts, Modals, Cards
+
+#### `/js/app.js`
+- Frontend-State-Management
+- AJAX-Calls zu allen API-Endpunkten
+- Modal-Handling
+- Toast-Notifications
+- Auto-Refresh (alle 30 Sekunden)
+
+---
+
+### `/data/` - Datenspeicher
+
+**Sicherheit**: `.htaccess` verweigert jeden Web-Zugriff!
+
+#### `config.json`
+```json
+{
+ "repositories": [
+ {
+ "id": "repo_uniqueid123",
+ "name": "Meine Website",
+ "repo_url": "https://github.com/user/repo.git",
+ "branch": "main",
+ "target_path": "/var/www/website",
+ "auto_sync": true,
+ "status": "synced",
+ "created_at": "2025-12-06 10:00:00",
+ "last_sync": "2025-12-06 14:30:00",
+ "last_commit": "abc123def456..."
+ }
+ ]
+}
+```
+
+#### `log.json`
+```json
+{
+ "entries": [
+ {
+ "id": "log_uniqueid456",
+ "timestamp": "2025-12-06 14:30:00",
+ "repo_id": "repo_uniqueid123",
+ "type": "success",
+ "message": "Pull completed successfully",
+ "details": {
+ "files_changed": 3,
+ "old_commit": "abc123d",
+ "new_commit": "def456a"
+ }
+ }
+ ]
+}
+```
+
+#### `secrets.json`
+```json
+{
+ "github_pat": "ghp_YourPersonalAccessTokenHere",
+ "webhook_secrets": {
+ "repo_uniqueid123": "generatedWebhookSecretHere"
+ }
+}
+```
+
+**Berechtigungen**: `chmod 600` (nur Owner kann lesen/schreiben)
+
+---
+
+### `/src/` - Backend-Klassen
+
+**Sicherheit**: `.htaccess` verweigert jeden Web-Zugriff!
+
+#### `ConfigManager.php`
+**Verantwortlichkeiten:**
+- JSON-Dateien lesen/schreiben
+- Repository-CRUD-Operationen
+- Webhook-Secret-Verwaltung
+- GitHub PAT verwalten
+
+**Wichtige Methoden:**
+```php
+getRepositories() // Alle Repos
+getRepository($id) // Einzelnes Repo
+addRepository($data) // Neues Repo
+updateRepository($id, $updates) // Repo aktualisieren
+deleteRepository($id) // Repo löschen
+getGitHubToken() // PAT abrufen
+setWebhookSecret($id, $secret) // Webhook Secret speichern
+```
+
+#### `Logger.php`
+**Verantwortlichkeiten:**
+- Log-Einträge erstellen
+- Logs nach Typ/Repo filtern
+- Statistiken generieren
+- Auto-Bereinigung (max. 1000 Einträge)
+
+**Wichtige Methoden:**
+```php
+success($repoId, $message, $details) // ✅ Erfolg loggen
+error($repoId, $message, $details) // ❌ Fehler loggen
+warning($repoId, $message, $details) // ⚠️ Warnung loggen
+info($repoId, $message, $details) // ℹ️ Info loggen
+getAll($limit, $offset) // Alle Logs
+getByRepository($repoId) // Logs für Repo
+getStats() // Statistiken
+```
+
+#### `GitHandler.php`
+**Verantwortlichkeiten:**
+- Git-Befehle ausführen
+- Repository klonen
+- Pull durchführen
+- Revert zu älterem Commit
+- Commit-Historie abrufen
+- Merge-Konflikte erkennen
+
+**Wichtige Methoden:**
+```php
+cloneRepository($repoId, $url, $path, $branch) // Initial clone
+pull($repoId, $path, $branch) // git pull
+revert($repoId, $path, $commitHash) // git revert
+getCurrentCommit($path) // Aktueller Commit
+getCommitHistory($path, $limit) // Commit-Liste
+getStatus($path) // git status
+getRemoteBranches($url) // Verfügbare Branches
+```
+
+**Sicherheit:**
+- Alle Shell-Befehle mit `escapeshellarg()` escaped
+- GitHub PAT wird in URL eingebettet für Auth
+- Fehlerbehandlung mit Try-Catch
+
+---
+
+## 🔐 Sicherheitskonzept
+
+### 1. Zugriffskontrolle
+
+**Web-zugänglich**: Nur `/public/`
+
+**Blockiert**:
+- `/data/` (enthält Secrets & Konfiguration)
+- `/src/` (PHP-Klassen)
+- Alle `.json` Dateien
+- `.git` Verzeichnisse
+
+### 2. Webhook-Sicherheit
+
+- HMAC SHA-256 Signatur-Verifizierung
+- Unique Secret pro Repository
+- Timing-Safe Vergleich (`hash_equals()`)
+
+### 3. Datei-Berechtigungen
+
+```bash
+/gitpusher/ 755 (www-data:www-data)
+/gitpusher/public/ 755
+/gitpusher/data/ 755
+/gitpusher/data/*.json 600 (nur Owner lesen/schreiben)
+/gitpusher/src/ 755
+```
+
+### 4. Input-Validierung
+
+- Alle User-Inputs werden validiert
+- JSON-Parsing mit Fehlerbehandlung
+- Repository-URLs werden geprüft
+- SQL-Injection nicht möglich (keine DB)
+- XSS-Prevention durch `escapeHtml()` im Frontend
+
+### 5. Git-Sicherheit
+
+- Alle Git-Befehle laufen als `www-data` User
+- Shell-Injection-Prevention durch `escapeshellarg()`
+- GitHub PAT mit minimalen Berechtigungen (nur `repo`)
+
+---
+
+## 🔄 Datenfluss
+
+### Automatischer Sync (Webhook)
+
+```
+GitHub Push Event
+ ↓
+ webhook.php
+ ↓
+1. Payload empfangen
+2. JSON dekodieren
+3. Signature verifizieren (HMAC SHA-256)
+4. Repository in Config finden
+5. Branch prüfen
+ ↓
+ GitHandler::pull()
+ ↓
+1. Aktuellen Commit speichern
+2. git pull ausführen
+3. Auf Merge-Konflikte prüfen
+4. Geänderte Dateien zählen
+ ↓
+ Logger::success/error()
+ ↓
+ ConfigManager::updateRepository()
+ ↓
+Status aktualisiert in config.json
+```
+
+### Manueller Sync
+
+```
+User klickt "Sync"-Button
+ ↓
+JavaScript: syncRepository(repoId)
+ ↓
+AJAX POST → api/sync.php
+ ↓
+GitHandler::pull()
+ ↓
+Logger::log()
+ ↓
+Response → JavaScript
+ ↓
+Dashboard-Refresh
+```
+
+### Repository hinzufügen
+
+```
+User füllt Formular aus
+ ↓
+JavaScript: addRepository(event)
+ ↓
+AJAX POST → api/repos.php
+ ↓
+1. Input validieren
+2. ConfigManager::addRepository()
+3. Webhook Secret generieren
+4. GitHandler::cloneRepository()
+ ↓
+5. Logger::success()
+6. Webhook-Info zurückgeben
+ ↓
+Modal mit Webhook-Daten anzeigen
+```
+
+---
+
+## 📊 Abhängigkeiten
+
+### PHP-Klassen
+
+```
+webhook.php
+├── ConfigManager
+├── Logger
+└── GitHandler
+ └── Logger
+ └── ConfigManager
+
+api/repos.php
+├── ConfigManager
+├── Logger
+└── GitHandler
+
+api/sync.php
+├── ConfigManager
+├── Logger
+└── GitHandler
+
+api/rollback.php
+├── ConfigManager
+├── Logger
+└── GitHandler
+
+api/log.php
+├── ConfigManager
+└── Logger
+```
+
+### Frontend
+
+```
+index.php (HTML)
+├── css/style.css
+└── js/app.js
+ ├── Fetch API (AJAX)
+ └── REST API Endpoints
+ ├── api/repos.php
+ ├── api/sync.php
+ ├── api/rollback.php
+ └── api/log.php
+```
+
+---
+
+## 🧪 Test-Checklist
+
+- [ ] Repository hinzufügen funktioniert
+- [ ] Initial Clone erfolgreich
+- [ ] Webhook empfängt Push-Events
+- [ ] Signatur-Verifizierung funktioniert
+- [ ] Manueller Sync funktioniert
+- [ ] Merge-Konflikte werden erkannt
+- [ ] Rollback erstellt Revert-Commit
+- [ ] Logs werden korrekt geschrieben
+- [ ] Dashboard zeigt Status korrekt
+- [ ] Repository löschen funktioniert
+- [ ] .htaccess blockiert /data/ Zugriff
+- [ ] .htaccess blockiert /src/ Zugriff
+
+---
+
+## 📚 Erweiterungsmöglichkeiten
+
+### Mögliche Features
+
+1. **Multi-User Support**
+ - User-Login
+ - Rollen-System (Admin, User)
+ - Repository-Berechtigungen pro User
+
+2. **E-Mail Benachrichtigungen**
+ - Bei erfolgreicher Sync
+ - Bei Fehlern/Konflikten
+ - Tägliche Zusammenfassung
+
+3. **Deployment Scripts**
+ - Post-Sync Hooks (z.B. `npm install`, `composer install`)
+ - Custom Shell-Befehle
+ - Build-Prozesse
+
+4. **Advanced Git Features**
+ - Submodules Support
+ - Tag/Release Tracking
+ - Multi-Branch Sync
+
+5. **Monitoring & Alerts**
+ - Prometheus Metrics
+ - Grafana Dashboard
+ - Slack/Discord Webhooks
+
+6. **API Authentication**
+ - API Keys
+ - JWT Tokens
+ - Rate Limiting
+
+7. **Backup System**
+ - Automatische Backups vor Sync
+ - Snapshot-Verwaltung
+ - Restore-Funktion
+
+---
+
+**Version**: 1.0.0
+**Erstellt**: 2025-12-06
+**Autor**: Claude Code
diff --git a/gitpusher/README.md b/gitpusher/README.md
new file mode 100644
index 0000000..f2b2bc3
--- /dev/null
+++ b/gitpusher/README.md
@@ -0,0 +1,341 @@
+# GitHub Sync - Automatische Repository-Synchronisation
+
+Eine einfache und sichere Lösung zur automatischen Synchronisation von GitHub-Repositories auf deinem Server mittels Webhooks.
+
+## 📋 Features
+
+- ✅ **Automatische Synchronisation** via GitHub Webhooks
+- ✅ **Mehrere Repositories** gleichzeitig verwalten
+- ✅ **Branch-Auswahl** pro Repository
+- ✅ **Webhook-Sicherheit** mit Secret-Verifizierung
+- ✅ **Manueller Sync** über das Dashboard
+- ✅ **Rollback-Funktion** via `git revert`
+- ✅ **Konflikt-Erkennung** mit Warnungen
+- ✅ **Log-System** für alle Ereignisse
+- ✅ **Datei-basiert** - keine Datenbank erforderlich
+- ✅ **Responsives Dashboard** mit Echtzeit-Updates
+
+## 🔧 Systemanforderungen
+
+- **Server**: Ubuntu Server (LXC Container auf Proxmox)
+- **Webserver**: Apache 2.4+
+- **PHP**: 7.4+ (8.0+ empfohlen)
+- **Git**: 2.0+
+- **Berechtigungen**: Root-Zugriff für Installation
+
+## 📦 Installation
+
+### 1. Voraussetzungen prüfen
+
+```bash
+# PHP Version prüfen
+php -v
+
+# Git Version prüfen
+git --version
+
+# Apache Status prüfen
+systemctl status apache2
+```
+
+### 2. Benötigte PHP-Module installieren
+
+```bash
+sudo apt update
+sudo apt install php php-cli php-json php-mbstring
+```
+
+### 3. Apache-Konfiguration
+
+#### Virtual Host erstellen
+
+```bash
+sudo nano /etc/apache2/sites-available/github-sync.conf
+```
+
+Füge folgende Konfiguration ein:
+
+```apache
+
+ ServerName github-sync.deine-domain.de
+ DocumentRoot /gitpusher/public
+
+
+ Options -Indexes +FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+ # Deny access to data and src directories
+
+ Require all denied
+
+
+
+ Require all denied
+
+
+ ErrorLog ${APACHE_LOG_DIR}/github-sync-error.log
+ CustomLog ${APACHE_LOG_DIR}/github-sync-access.log combined
+
+```
+
+#### Site aktivieren
+
+```bash
+sudo a2ensite github-sync.conf
+sudo a2enmod rewrite
+sudo systemctl reload apache2
+```
+
+### 4. Berechtigungen setzen
+
+```bash
+# Eigentümer auf www-data setzen
+sudo chown -R www-data:www-data /gitpusher
+
+# Schreibrechte für data-Verzeichnis
+sudo chmod 755 /gitpusher/data
+sudo chmod 600 /gitpusher/data/*.json
+
+# Ausführrechte für public-Verzeichnis
+sudo chmod 755 /gitpusher/public
+```
+
+### 5. GitHub Personal Access Token erstellen
+
+1. Gehe zu GitHub → Settings → Developer settings → Personal access tokens
+2. Klicke auf "Generate new token (classic)"
+3. Name: `GitHub Sync Server`
+4. Wähle Scopes:
+ - ✅ `repo` (Full control of private repositories)
+5. Klicke auf "Generate token"
+6. **Kopiere den Token sofort** - er wird nur einmal angezeigt!
+
+### 6. Token in der Anwendung hinterlegen
+
+Bearbeite `/gitpusher/data/secrets.json`:
+
+```bash
+sudo nano /gitpusher/data/secrets.json
+```
+
+Füge deinen GitHub PAT ein:
+
+```json
+{
+ "github_pat": "ghp_deinTokenHier1234567890",
+ "webhook_secrets": {}
+}
+```
+
+Speichern mit `Ctrl+O`, beenden mit `Ctrl+X`.
+
+## 🚀 Verwendung
+
+### Dashboard öffnen
+
+Öffne deinen Browser und navigiere zu:
+```
+http://github-sync.deine-domain.de
+```
+
+### Repository hinzufügen
+
+1. Klicke auf **"+ Repository hinzufügen"**
+2. Fülle das Formular aus:
+ - **Name**: Ein aussagekräftiger Name (z.B. "Meine Website")
+ - **GitHub Repository URL**: `https://github.com/user/repo.git`
+ - **Branch**: z.B. `main` oder `master`
+ - **Ziel-Pfad**: z.B. `/var/www/meine-website`
+ - **Auto-Sync**: Aktiviert für automatische Webhooks
+3. Klicke auf **"Repository hinzufügen"**
+
+Die App klont das Repository automatisch und zeigt dir die Webhook-Konfiguration an.
+
+### GitHub Webhook einrichten
+
+1. Gehe zu deinem GitHub Repository → **Settings** → **Webhooks** → **Add webhook**
+2. Füge die Informationen aus dem Modal ein:
+ - **Payload URL**: (aus dem Modal kopieren)
+ - **Content type**: `application/json`
+ - **Secret**: (aus dem Modal kopieren)
+ - **Events**: "Just the push event"
+3. Klicke auf **"Add webhook"**
+
+Ab jetzt wird bei jedem Push automatisch synchronisiert!
+
+### Manueller Sync
+
+Klicke auf den Button **"🔄 Manueller Sync"** bei einem Repository, um sofort zu synchronisieren.
+
+### Rollback durchführen
+
+1. Klicke auf **"⏪ Rollback"** bei einem Repository
+2. Wähle den Commit aus, zu dem du zurückkehren möchtest
+3. Bestätige die Aktion
+
+**Wichtig**: Es wird ein neuer Revert-Commit erstellt, keine Commits werden gelöscht!
+
+### Repository entfernen
+
+1. Klicke auf **"🗑️ Entfernen"**
+2. Wähle, ob auch die Dateien gelöscht werden sollen
+3. Bestätige die Aktion
+
+## 📁 Dateistruktur
+
+```
+/gitpusher/
+├── public/ # Web-Root (Apache DocumentRoot)
+│ ├── index.php # Dashboard
+│ ├── webhook.php # Webhook-Endpoint
+│ ├── api/
+│ │ ├── repos.php # Repository-Verwaltung
+│ │ ├── sync.php # Manueller Sync
+│ │ ├── rollback.php # Rollback-Funktion
+│ │ └── log.php # Logs abrufen
+│ ├── css/
+│ │ └── style.css # Styling
+│ └── js/
+│ └── app.js # Frontend-Logik
+│
+├── data/ # Daten (nicht web-zugänglich)
+│ ├── config.json # Repository-Konfiguration
+│ ├── log.json # Log-Einträge
+│ ├── secrets.json # GitHub PAT & Webhook Secrets
+│ └── .htaccess # Zugriff verweigern
+│
+├── src/ # PHP-Klassen
+│ ├── ConfigManager.php # Konfigurationsverwaltung
+│ ├── Logger.php # Logging
+│ ├── GitHandler.php # Git-Operationen
+│ └── .htaccess # Zugriff verweigern
+│
+├── .htaccess # Hauptkonfiguration
+└── README.md # Diese Datei
+```
+
+## 🔒 Sicherheit
+
+### Webhook-Signatur-Verifizierung
+
+Alle Webhooks werden mit HMAC SHA-256 signiert und verifiziert. Ohne gültiges Secret werden Requests abgelehnt.
+
+### Datei-Berechtigungen
+
+- `/gitpusher/data/`: Nur von PHP lesbar (600)
+- `/gitpusher/src/`: Nicht web-zugänglich
+- `.htaccess`: Schützt sensitive Verzeichnisse
+
+### GitHub PAT
+
+- Wird verschlüsselt in `secrets.json` gespeichert
+- Nur `repo`-Scope erforderlich
+- Kann jederzeit in GitHub widerrufen werden
+
+## 🐛 Troubleshooting
+
+### "Permission denied" beim Clonen
+
+```bash
+# Stelle sicher, dass www-data Schreibrechte hat
+sudo chown -R www-data:www-data /var/www
+sudo chmod 755 /var/www
+```
+
+### Webhook wird nicht empfangen
+
+1. Prüfe GitHub Webhook Deliveries auf Fehler
+2. Überprüfe Apache Error Log:
+ ```bash
+ sudo tail -f /var/log/apache2/github-sync-error.log
+ ```
+3. Teste Webhook manuell:
+ ```bash
+ curl -X POST http://github-sync.deine-domain.de/webhook.php \
+ -H "Content-Type: application/json" \
+ -d '{"repository":{"clone_url":"https://github.com/user/repo.git"}}'
+ ```
+
+### Merge-Konflikt
+
+Bei Konflikten zeigt das Dashboard eine Warnung. Löse den Konflikt manuell:
+
+```bash
+cd /var/www/dein-repo
+sudo -u www-data git status
+# Konflikt manuell lösen
+sudo -u www-data git add .
+sudo -u www-data git commit -m "Konflikt gelöst"
+```
+
+### Logs prüfen
+
+```bash
+# PHP Error Log
+sudo tail -f /var/log/apache2/error.log
+
+# App Logs
+cat /gitpusher/data/log.json | jq
+```
+
+## 📊 API-Endpunkte
+
+### GET /api/repos.php
+Listet alle Repositories auf
+
+### POST /api/repos.php
+Fügt neues Repository hinzu
+
+### PUT /api/repos.php
+Aktualisiert Repository
+
+### DELETE /api/repos.php
+Löscht Repository
+
+### POST /api/sync.php
+Führt manuellen Sync durch
+
+### GET /api/rollback.php
+Listet Commits für Rollback
+
+### POST /api/rollback.php
+Führt Rollback durch
+
+### GET /api/log.php
+Ruft Logs ab
+
+### POST /webhook.php
+Empfängt GitHub Webhooks
+
+## 🔄 Updates
+
+Um das System zu aktualisieren:
+
+1. Backup erstellen:
+ ```bash
+ sudo cp -r /gitpusher/data /gitpusher/data.backup
+ ```
+
+2. Neue Dateien deployen
+
+3. Berechtigungen prüfen:
+ ```bash
+ sudo chown -R www-data:www-data /gitpusher
+ ```
+
+## 📝 Lizenz
+
+Dieses Projekt ist für den persönlichen und kommerziellen Gebrauch frei verfügbar.
+
+## 🙋 Support
+
+Bei Fragen oder Problemen:
+1. Prüfe die Logs im Dashboard
+2. Prüfe Apache Error Logs
+3. Prüfe GitHub Webhook Delivery Logs
+
+---
+
+Erstellt mit ❤️ für einfache GitHub-Synchronisation
diff --git a/gitpusher/data/.htaccess b/gitpusher/data/.htaccess
new file mode 100644
index 0000000..ff502da
--- /dev/null
+++ b/gitpusher/data/.htaccess
@@ -0,0 +1,8 @@
+# Deny all access to data directory
+Require all denied
+
+# Alternative for older Apache versions
+
+ Order deny,allow
+ Deny from all
+
diff --git a/gitpusher/public/api/log.php b/gitpusher/public/api/log.php
new file mode 100644
index 0000000..77da046
--- /dev/null
+++ b/gitpusher/public/api/log.php
@@ -0,0 +1,44 @@
+ 'Method not allowed']);
+ exit;
+}
+
+$configManager = new ConfigManager();
+$logger = new Logger($configManager);
+
+// Get query parameters
+$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 100;
+$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
+$repoId = $_GET['repo_id'] ?? null;
+$type = $_GET['type'] ?? null;
+
+// Get logs based on filters
+if ($repoId) {
+ $logs = $logger->getByRepository($repoId, $limit);
+} elseif ($type) {
+ $logs = $logger->getByType($type, $limit);
+} else {
+ $logs = $logger->getAll($limit, $offset);
+}
+
+// Get statistics
+$stats = $logger->getStats();
+
+echo json_encode([
+ 'success' => true,
+ 'logs' => $logs,
+ 'stats' => $stats,
+ 'count' => count($logs)
+]);
diff --git a/gitpusher/public/api/repos.php b/gitpusher/public/api/repos.php
new file mode 100644
index 0000000..9186ebb
--- /dev/null
+++ b/gitpusher/public/api/repos.php
@@ -0,0 +1,245 @@
+getRepository($_GET['id']);
+
+ if (!$repo) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Repository not found']);
+ exit;
+ }
+
+ // Add current git status if path exists
+ if (file_exists($repo['target_path'])) {
+ $status = $gitHandler->getStatus($repo['target_path']);
+ $repo['git_status'] = $status;
+ $repo['current_branch'] = $gitHandler->getCurrentBranch($repo['target_path']);
+ $repo['current_commit'] = $gitHandler->getCurrentCommit($repo['target_path']);
+ }
+
+ echo json_encode($repo);
+ } else {
+ $repos = $configManager->getRepositories();
+
+ // Add status for each repo
+ foreach ($repos as &$repo) {
+ if (file_exists($repo['target_path'])) {
+ $repo['exists'] = true;
+ $repo['current_branch'] = $gitHandler->getCurrentBranch($repo['target_path']);
+ } else {
+ $repo['exists'] = false;
+ }
+ }
+
+ echo json_encode(['repositories' => $repos]);
+ }
+ exit;
+}
+
+// POST - Add new repository
+if ($method === 'POST') {
+ $input = json_decode(file_get_contents('php://input'), true);
+
+ // Validate required fields
+ $required = ['name', 'repo_url', 'target_path', 'branch'];
+ foreach ($required as $field) {
+ if (empty($input[$field])) {
+ http_response_code(400);
+ echo json_encode(['error' => "Field '$field' is required"]);
+ exit;
+ }
+ }
+
+ // Validate target path
+ $targetPath = rtrim($input['target_path'], '/');
+
+ // Check if target path already exists
+ if (file_exists($targetPath)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Target path already exists']);
+ exit;
+ }
+
+ // Validate repository URL format
+ if (!filter_var($input['repo_url'], FILTER_VALIDATE_URL)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Invalid repository URL']);
+ exit;
+ }
+
+ // Generate webhook secret
+ $webhookSecret = $configManager->generateWebhookSecret();
+
+ // Prepare repository data
+ $repoData = [
+ 'name' => $input['name'],
+ 'repo_url' => $input['repo_url'],
+ 'target_path' => $targetPath,
+ 'branch' => $input['branch'],
+ 'auto_sync' => $input['auto_sync'] ?? true,
+ 'status' => 'cloning'
+ ];
+
+ // Add repository to config
+ $repoId = $configManager->addRepository($repoData);
+
+ if (!$repoId) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to add repository']);
+ exit;
+ }
+
+ // Save webhook secret
+ $configManager->setWebhookSecret($repoId, $webhookSecret);
+
+ // Clone repository
+ $result = $gitHandler->cloneRepository(
+ $repoId,
+ $input['repo_url'],
+ $targetPath,
+ $input['branch']
+ );
+
+ if ($result['success']) {
+ $configManager->updateRepository($repoId, [
+ 'status' => 'synced',
+ 'last_sync' => date('Y-m-d H:i:s')
+ ]);
+
+ $repo = $configManager->getRepository($repoId);
+ $repo['webhook_secret'] = $webhookSecret;
+ $repo['webhook_url'] = (isset($_SERVER['HTTPS']) ? 'https' : 'http') .
+ '://' . $_SERVER['HTTP_HOST'] .
+ dirname(dirname($_SERVER['REQUEST_URI'])) . '/webhook.php';
+
+ echo json_encode([
+ 'success' => true,
+ 'repository' => $repo
+ ]);
+ } else {
+ $configManager->updateRepository($repoId, ['status' => 'error']);
+
+ http_response_code(500);
+ echo json_encode([
+ 'success' => false,
+ 'error' => $result['message'],
+ 'details' => $result['error'] ?? null
+ ]);
+ }
+
+ exit;
+}
+
+// PUT - Update repository
+if ($method === 'PUT') {
+ $input = json_decode(file_get_contents('php://input'), true);
+
+ if (empty($input['id'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository ID is required']);
+ exit;
+ }
+
+ $repo = $configManager->getRepository($input['id']);
+
+ if (!$repo) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Repository not found']);
+ exit;
+ }
+
+ // Prepare updates (only allow certain fields to be updated)
+ $allowedFields = ['name', 'branch', 'auto_sync'];
+ $updates = [];
+
+ foreach ($allowedFields as $field) {
+ if (isset($input[$field])) {
+ $updates[$field] = $input[$field];
+ }
+ }
+
+ if (empty($updates)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'No valid fields to update']);
+ exit;
+ }
+
+ $success = $configManager->updateRepository($input['id'], $updates);
+
+ if ($success) {
+ $repo = $configManager->getRepository($input['id']);
+ echo json_encode([
+ 'success' => true,
+ 'repository' => $repo
+ ]);
+ } else {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to update repository']);
+ }
+
+ exit;
+}
+
+// DELETE - Delete repository
+if ($method === 'DELETE') {
+ $input = json_decode(file_get_contents('php://input'), true);
+
+ if (empty($input['id'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository ID is required']);
+ exit;
+ }
+
+ $repo = $configManager->getRepository($input['id']);
+
+ if (!$repo) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Repository not found']);
+ exit;
+ }
+
+ // Delete repository from config
+ $success = $configManager->deleteRepository($input['id']);
+
+ if ($success) {
+ $logger->info($input['id'], "Repository removed from configuration");
+
+ // Optionally delete files if requested
+ if (!empty($input['delete_files']) && file_exists($repo['target_path'])) {
+ exec('rm -rf ' . escapeshellarg($repo['target_path']));
+ $logger->info($input['id'], "Repository files deleted from disk");
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'message' => 'Repository deleted'
+ ]);
+ } else {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to delete repository']);
+ }
+
+ exit;
+}
+
+// Method not allowed
+http_response_code(405);
+echo json_encode(['error' => 'Method not allowed']);
diff --git a/gitpusher/public/api/rollback.php b/gitpusher/public/api/rollback.php
new file mode 100644
index 0000000..c3b2a76
--- /dev/null
+++ b/gitpusher/public/api/rollback.php
@@ -0,0 +1,107 @@
+ 'Repository ID is required']);
+ exit;
+ }
+
+ $repo = $configManager->getRepository($_GET['repo_id']);
+
+ if (!$repo) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Repository not found']);
+ exit;
+ }
+
+ if (!file_exists($repo['target_path'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository path does not exist']);
+ exit;
+ }
+
+ $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
+ $commits = $gitHandler->getCommitHistory($repo['target_path'], $limit);
+
+ echo json_encode([
+ 'success' => true,
+ 'commits' => $commits
+ ]);
+ exit;
+}
+
+// POST - Perform rollback
+if ($method === 'POST') {
+ $input = json_decode(file_get_contents('php://input'), true);
+
+ if (empty($input['repo_id'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository ID is required']);
+ exit;
+ }
+
+ if (empty($input['commit_hash'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Commit hash is required']);
+ exit;
+ }
+
+ $repo = $configManager->getRepository($input['repo_id']);
+
+ if (!$repo) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Repository not found']);
+ exit;
+ }
+
+ if (!file_exists($repo['target_path'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository path does not exist']);
+ exit;
+ }
+
+ // Perform revert
+ $result = $gitHandler->revert(
+ $repo['id'],
+ $repo['target_path'],
+ $input['commit_hash']
+ );
+
+ if ($result['success']) {
+ echo json_encode([
+ 'success' => true,
+ 'message' => $result['message'],
+ 'output' => $result['output']
+ ]);
+ } else {
+ http_response_code(400);
+ echo json_encode([
+ 'success' => false,
+ 'message' => $result['message'],
+ 'error' => $result['error'] ?? null
+ ]);
+ }
+ exit;
+}
+
+// Method not allowed
+http_response_code(405);
+echo json_encode(['error' => 'Method not allowed']);
diff --git a/gitpusher/public/api/sync.php b/gitpusher/public/api/sync.php
new file mode 100644
index 0000000..f7a13e0
--- /dev/null
+++ b/gitpusher/public/api/sync.php
@@ -0,0 +1,71 @@
+ 'Method not allowed']);
+ exit;
+}
+
+$input = json_decode(file_get_contents('php://input'), true);
+
+if (empty($input['repo_id'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository ID is required']);
+ exit;
+}
+
+$configManager = new ConfigManager();
+$logger = new Logger($configManager);
+$gitHandler = new GitHandler($logger, $configManager);
+
+$repo = $configManager->getRepository($input['repo_id']);
+
+if (!$repo) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Repository not found']);
+ exit;
+}
+
+// Check if repository path exists
+if (!file_exists($repo['target_path'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository path does not exist. Please clone first.']);
+ exit;
+}
+
+// Perform sync
+$logger->info($repo['id'], "Manual sync triggered");
+
+$result = $gitHandler->pull(
+ $repo['id'],
+ $repo['target_path'],
+ $repo['branch']
+);
+
+if ($result['success']) {
+ echo json_encode([
+ 'success' => true,
+ 'message' => $result['message'],
+ 'files_changed' => $result['files_changed'] ?? 0,
+ 'output' => $result['output']
+ ]);
+} else {
+ http_response_code(400);
+ echo json_encode([
+ 'success' => false,
+ 'message' => $result['message'],
+ 'conflict' => $result['conflict'] ?? false,
+ 'error' => $result['error'] ?? null,
+ 'output' => $result['output'] ?? null
+ ]);
+}
diff --git a/gitpusher/public/css/style.css b/gitpusher/public/css/style.css
new file mode 100644
index 0000000..1d9b1f4
--- /dev/null
+++ b/gitpusher/public/css/style.css
@@ -0,0 +1,633 @@
+/* Reset and Base Styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --primary-color: #0366d6;
+ --success-color: #28a745;
+ --warning-color: #ffc107;
+ --error-color: #dc3545;
+ --info-color: #17a2b8;
+ --bg-color: #f6f8fa;
+ --card-bg: #ffffff;
+ --text-primary: #24292e;
+ --text-secondary: #586069;
+ --border-color: #e1e4e8;
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
+ --shadow-hover: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-primary);
+ line-height: 1.6;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+/* Header */
+header {
+ text-align: center;
+ padding: 40px 20px;
+ background: linear-gradient(135deg, var(--primary-color), #0550ae);
+ color: white;
+ border-radius: 8px;
+ margin-bottom: 30px;
+ box-shadow: var(--shadow);
+}
+
+header h1 {
+ font-size: 2.5em;
+ margin-bottom: 10px;
+}
+
+.subtitle {
+ font-size: 1.1em;
+ opacity: 0.9;
+}
+
+/* Stats Grid */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ background: var(--card-bg);
+ padding: 25px;
+ border-radius: 8px;
+ box-shadow: var(--shadow);
+ text-align: center;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-hover);
+}
+
+.stat-value {
+ font-size: 2.5em;
+ font-weight: bold;
+ color: var(--primary-color);
+ margin-bottom: 5px;
+}
+
+.stat-label {
+ font-size: 0.9em;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Sections */
+.section {
+ background: var(--card-bg);
+ padding: 25px;
+ border-radius: 8px;
+ box-shadow: var(--shadow);
+ margin-bottom: 20px;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 2px solid var(--border-color);
+}
+
+.section-header h2 {
+ font-size: 1.5em;
+ color: var(--text-primary);
+}
+
+/* Buttons */
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow);
+}
+
+.btn-primary {
+ background: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: #0550ae;
+}
+
+.btn-secondary {
+ background: var(--text-secondary);
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #444d56;
+}
+
+.btn-success {
+ background: var(--success-color);
+ color: white;
+}
+
+.btn-warning {
+ background: var(--warning-color);
+ color: #212529;
+}
+
+.btn-danger {
+ background: var(--error-color);
+ color: white;
+}
+
+.btn-sm {
+ padding: 5px 12px;
+ font-size: 12px;
+}
+
+/* Repository List */
+.repos-list {
+ display: grid;
+ gap: 15px;
+}
+
+.repo-card {
+ background: var(--bg-color);
+ padding: 20px;
+ border-radius: 8px;
+ border-left: 4px solid var(--primary-color);
+ transition: all 0.2s;
+}
+
+.repo-card:hover {
+ box-shadow: var(--shadow);
+}
+
+.repo-card.status-synced {
+ border-left-color: var(--success-color);
+}
+
+.repo-card.status-error {
+ border-left-color: var(--error-color);
+}
+
+.repo-card.status-conflict {
+ border-left-color: var(--warning-color);
+}
+
+.repo-card.status-cloning {
+ border-left-color: var(--info-color);
+}
+
+.repo-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 15px;
+}
+
+.repo-name {
+ font-size: 1.3em;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 5px;
+}
+
+.repo-url {
+ font-size: 0.9em;
+ color: var(--text-secondary);
+ font-family: 'Courier New', monospace;
+}
+
+.repo-status {
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.repo-status.synced {
+ background: #d4edda;
+ color: #155724;
+}
+
+.repo-status.error {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.repo-status.conflict {
+ background: #fff3cd;
+ color: #856404;
+}
+
+.repo-status.cloning {
+ background: #d1ecf1;
+ color: #0c5460;
+}
+
+.repo-info {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 15px;
+ margin-bottom: 15px;
+ padding: 15px;
+ background: white;
+ border-radius: 6px;
+}
+
+.info-item {
+ display: flex;
+ flex-direction: column;
+}
+
+.info-label {
+ font-size: 0.8em;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 3px;
+}
+
+.info-value {
+ font-family: 'Courier New', monospace;
+ font-size: 0.9em;
+ color: var(--text-primary);
+}
+
+.repo-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+/* Logs List */
+.logs-list {
+ max-height: 500px;
+ overflow-y: auto;
+}
+
+.log-entry {
+ padding: 12px;
+ border-left: 3px solid var(--info-color);
+ background: var(--bg-color);
+ margin-bottom: 10px;
+ border-radius: 4px;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 15px;
+ align-items: start;
+}
+
+.log-entry.success {
+ border-left-color: var(--success-color);
+}
+
+.log-entry.error {
+ border-left-color: var(--error-color);
+}
+
+.log-entry.warning {
+ border-left-color: var(--warning-color);
+}
+
+.log-entry.info {
+ border-left-color: var(--info-color);
+}
+
+.log-timestamp {
+ font-size: 0.85em;
+ color: var(--text-secondary);
+ font-family: 'Courier New', monospace;
+ white-space: nowrap;
+}
+
+.log-content {
+ flex: 1;
+}
+
+.log-message {
+ color: var(--text-primary);
+ margin-bottom: 5px;
+}
+
+.log-details {
+ font-size: 0.85em;
+ color: var(--text-secondary);
+ font-family: 'Courier New', monospace;
+}
+
+/* Modal */
+.modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal.active {
+ display: flex;
+}
+
+.modal-content {
+ background: var(--card-bg);
+ border-radius: 8px;
+ max-width: 600px;
+ width: 90%;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 1.5em;
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ font-size: 2em;
+ cursor: pointer;
+ color: var(--text-secondary);
+ line-height: 1;
+ padding: 0;
+ width: 30px;
+ height: 30px;
+}
+
+.close-btn:hover {
+ color: var(--text-primary);
+}
+
+.modal-body {
+ padding: 20px;
+}
+
+/* Forms */
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.form-group input[type="text"],
+.form-group input[type="url"],
+.form-group select {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ font-size: 14px;
+ font-family: inherit;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
+}
+
+.form-group small {
+ display: block;
+ margin-top: 5px;
+ color: var(--text-secondary);
+ font-size: 0.85em;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+}
+
+.checkbox-label input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+.form-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ margin-top: 25px;
+ padding-top: 20px;
+ border-top: 1px solid var(--border-color);
+}
+
+.input-with-copy {
+ display: flex;
+ gap: 10px;
+}
+
+.input-with-copy input {
+ flex: 1;
+}
+
+/* Commits List */
+.commits-list {
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.commit-item {
+ padding: 15px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ margin-bottom: 10px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.commit-item:hover {
+ background: var(--bg-color);
+ border-color: var(--primary-color);
+}
+
+.commit-hash {
+ font-family: 'Courier New', monospace;
+ font-weight: 600;
+ color: var(--primary-color);
+ margin-bottom: 5px;
+}
+
+.commit-message {
+ color: var(--text-primary);
+ margin-bottom: 5px;
+}
+
+.commit-meta {
+ font-size: 0.85em;
+ color: var(--text-secondary);
+}
+
+/* Alert */
+.alert {
+ padding: 15px;
+ border-radius: 6px;
+ margin-bottom: 15px;
+}
+
+.alert-info {
+ background: #d1ecf1;
+ border: 1px solid #bee5eb;
+ color: #0c5460;
+}
+
+.alert-info ol {
+ margin: 10px 0 0 20px;
+}
+
+/* Toast Notifications */
+#toastContainer {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 2000;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.toast {
+ background: white;
+ padding: 15px 20px;
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ min-width: 300px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ animation: slideIn 0.3s ease;
+}
+
+.toast.success {
+ border-left: 4px solid var(--success-color);
+}
+
+.toast.error {
+ border-left: 4px solid var(--error-color);
+}
+
+.toast.warning {
+ border-left: 4px solid var(--warning-color);
+}
+
+.toast.info {
+ border-left: 4px solid var(--info-color);
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+/* Loading */
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: var(--text-secondary);
+}
+
+.loading::before {
+ content: "⏳ ";
+ font-size: 1.5em;
+}
+
+/* Empty State */
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: var(--text-secondary);
+}
+
+.empty-state::before {
+ content: "📁";
+ font-size: 4em;
+ display: block;
+ margin-bottom: 20px;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ header h1 {
+ font-size: 1.8em;
+ }
+
+ .section-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 15px;
+ }
+
+ .repo-header {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .repo-actions {
+ width: 100%;
+ }
+
+ .repo-actions .btn {
+ flex: 1;
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
diff --git a/gitpusher/public/index.php b/gitpusher/public/index.php
new file mode 100644
index 0000000..38deee4
--- /dev/null
+++ b/gitpusher/public/index.php
@@ -0,0 +1,181 @@
+
+
+
+
+
+ GitHub Sync - Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
0
+
Log-Einträge (24h)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Wähle einen Commit aus, zu dem du zurückkehren möchtest:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Einrichtung:
+
+ - Gehe zu deinem GitHub Repository
+ - Settings → Webhooks → Add webhook
+ - Füge die obige Payload URL ein
+ - Füge das Secret ein
+ - Wähle "application/json" als Content type
+ - Wähle "Just the push event"
+ - Klicke auf "Add webhook"
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gitpusher/public/js/app.js b/gitpusher/public/js/app.js
new file mode 100644
index 0000000..af71f43
--- /dev/null
+++ b/gitpusher/public/js/app.js
@@ -0,0 +1,516 @@
+/**
+ * GitHub Sync Dashboard - Frontend JavaScript
+ */
+
+// State
+let repositories = [];
+let logs = [];
+let stats = {};
+let currentRollbackRepoId = null;
+
+// Initialize app when DOM is loaded
+document.addEventListener('DOMContentLoaded', function() {
+ loadDashboard();
+ // Auto-refresh every 30 seconds
+ setInterval(loadDashboard, 30000);
+});
+
+/**
+ * Load complete dashboard
+ */
+async function loadDashboard() {
+ await Promise.all([
+ loadRepositories(),
+ loadLogs()
+ ]);
+ updateStats();
+}
+
+/**
+ * Load repositories
+ */
+async function loadRepositories() {
+ try {
+ const response = await fetch('api/repos.php');
+ const data = await response.json();
+
+ if (data.repositories) {
+ repositories = data.repositories;
+ renderRepositories();
+ }
+ } catch (error) {
+ console.error('Error loading repositories:', error);
+ showToast('Fehler beim Laden der Repositories', 'error');
+ }
+}
+
+/**
+ * Render repositories list
+ */
+function renderRepositories() {
+ const container = document.getElementById('reposList');
+
+ if (repositories.length === 0) {
+ container.innerHTML = 'Keine Repositories konfiguriert. Füge dein erstes Repository hinzu!
';
+ return;
+ }
+
+ container.innerHTML = repositories.map(repo => `
+
+
+
+
+
+ Branch
+ ${escapeHtml(repo.branch)}
+
+
+ Ziel-Pfad
+ ${escapeHtml(repo.target_path)}
+
+
+ Letzte Sync
+ ${repo.last_sync || 'Noch nie'}
+
+
+ Auto-Sync
+ ${repo.auto_sync ? '✅ Aktiv' : '❌ Inaktiv'}
+
+
+
+
+
+
+
+
+
+
+ `).join('');
+}
+
+/**
+ * Load logs
+ */
+async function loadLogs() {
+ try {
+ const response = await fetch('api/log.php?limit=50');
+ const data = await response.json();
+
+ if (data.success) {
+ logs = data.logs;
+ stats = data.stats;
+ renderLogs();
+ }
+ } catch (error) {
+ console.error('Error loading logs:', error);
+ showToast('Fehler beim Laden der Logs', 'error');
+ }
+}
+
+/**
+ * Render logs list
+ */
+function renderLogs() {
+ const container = document.getElementById('logsList');
+
+ if (logs.length === 0) {
+ container.innerHTML = 'Noch keine Log-Einträge vorhanden.
';
+ return;
+ }
+
+ container.innerHTML = logs.map(log => {
+ const repo = repositories.find(r => r.id === log.repo_id);
+ const repoName = repo ? repo.name : log.repo_id;
+
+ return `
+
+
${log.timestamp}
+
+
+ ${escapeHtml(repoName)} - ${escapeHtml(log.message)}
+
+ ${log.details && Object.keys(log.details).length > 0 ? `
+
${formatLogDetails(log.details)}
+ ` : ''}
+
+
+ `;
+ }).join('');
+}
+
+/**
+ * Update statistics
+ */
+function updateStats() {
+ const totalRepos = repositories.length;
+ const syncedRepos = repositories.filter(r => r.status === 'synced').length;
+ const errorRepos = repositories.filter(r => r.status === 'error' || r.status === 'conflict').length;
+
+ document.getElementById('totalRepos').textContent = totalRepos;
+ document.getElementById('syncedRepos').textContent = syncedRepos;
+ document.getElementById('errorRepos').textContent = errorRepos;
+ document.getElementById('totalLogs').textContent = stats.last_24h || 0;
+}
+
+/**
+ * Show add repository modal
+ */
+function showAddRepoModal() {
+ document.getElementById('addRepoModal').classList.add('active');
+}
+
+/**
+ * Add repository
+ */
+async function addRepository(event) {
+ event.preventDefault();
+
+ const form = event.target;
+ const formData = new FormData(form);
+
+ const data = {
+ name: formData.get('name'),
+ repo_url: formData.get('repo_url'),
+ branch: formData.get('branch'),
+ target_path: formData.get('target_path'),
+ auto_sync: formData.get('auto_sync') === 'on'
+ };
+
+ try {
+ const response = await fetch('api/repos.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Repository erfolgreich hinzugefügt!', 'success');
+ closeModal('addRepoModal');
+ form.reset();
+
+ // Show webhook info
+ if (result.repository.webhook_secret) {
+ showWebhookInfoData(result.repository.webhook_url, result.repository.webhook_secret);
+ }
+
+ loadDashboard();
+ } else {
+ showToast(result.error || 'Fehler beim Hinzufügen des Repositories', 'error');
+ }
+ } catch (error) {
+ console.error('Error adding repository:', error);
+ showToast('Fehler beim Hinzufügen des Repositories', 'error');
+ }
+}
+
+/**
+ * Sync repository manually
+ */
+async function syncRepository(repoId) {
+ const repo = repositories.find(r => r.id === repoId);
+
+ if (!repo) return;
+
+ showToast(`Synchronisiere ${repo.name}...`, 'info');
+
+ try {
+ const response = await fetch('api/sync.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ repo_id: repoId })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`${repo.name} erfolgreich synchronisiert! ${result.files_changed} Datei(en) geändert.`, 'success');
+ loadDashboard();
+ } else {
+ if (result.conflict) {
+ showToast(`Merge-Konflikt in ${repo.name}! Manuelle Lösung erforderlich.`, 'warning');
+ } else {
+ showToast(result.message || 'Fehler beim Synchronisieren', 'error');
+ }
+ loadDashboard();
+ }
+ } catch (error) {
+ console.error('Error syncing repository:', error);
+ showToast('Fehler beim Synchronisieren', 'error');
+ }
+}
+
+/**
+ * Show rollback modal
+ */
+async function showRollbackModal(repoId) {
+ currentRollbackRepoId = repoId;
+ const modal = document.getElementById('rollbackModal');
+ const commitsList = document.getElementById('commitsList');
+
+ modal.classList.add('active');
+ commitsList.innerHTML = 'Lade Commits...
';
+
+ try {
+ const response = await fetch(`api/rollback.php?repo_id=${repoId}&limit=20`);
+ const result = await response.json();
+
+ if (result.success && result.commits.length > 0) {
+ commitsList.innerHTML = result.commits.map(commit => `
+
+
${commit.hash_short}
+
${escapeHtml(commit.message)}
+
+ ${escapeHtml(commit.author_name)} - ${commit.timestamp}
+
+
+ `).join('');
+ } else {
+ commitsList.innerHTML = 'Keine Commits gefunden.
';
+ }
+ } catch (error) {
+ console.error('Error loading commits:', error);
+ commitsList.innerHTML = 'Fehler beim Laden der Commits.
';
+ }
+}
+
+/**
+ * Perform rollback
+ */
+async function performRollback(commitHash) {
+ if (!currentRollbackRepoId) return;
+
+ const repo = repositories.find(r => r.id === currentRollbackRepoId);
+
+ if (!confirm(`Möchtest du wirklich einen Rollback zu Commit ${commitHash.substring(0, 7)} durchführen?\n\nDies erstellt einen neuen Revert-Commit.`)) {
+ return;
+ }
+
+ showToast(`Führe Rollback durch...`, 'info');
+
+ try {
+ const response = await fetch('api/rollback.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ repo_id: currentRollbackRepoId,
+ commit_hash: commitHash
+ })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Rollback erfolgreich durchgeführt!', 'success');
+ closeModal('rollbackModal');
+ loadDashboard();
+ } else {
+ showToast(result.message || 'Fehler beim Rollback', 'error');
+ }
+ } catch (error) {
+ console.error('Error performing rollback:', error);
+ showToast('Fehler beim Rollback', 'error');
+ }
+}
+
+/**
+ * Show webhook info modal
+ */
+async function showWebhookInfo(repoId) {
+ const repo = repositories.find(r => r.id === repoId);
+
+ if (!repo) return;
+
+ // Fetch webhook secret from config
+ try {
+ const response = await fetch(`api/repos.php?id=${repoId}`);
+ const data = await response.json();
+
+ const webhookUrl = window.location.origin + window.location.pathname.replace('index.php', '') + 'webhook.php';
+ const webhookSecret = '(Secret gespeichert auf Server)';
+
+ showWebhookInfoData(webhookUrl, webhookSecret);
+ } catch (error) {
+ console.error('Error loading webhook info:', error);
+ }
+}
+
+/**
+ * Show webhook info with data
+ */
+function showWebhookInfoData(url, secret) {
+ document.getElementById('webhookUrl').value = url;
+ document.getElementById('webhookSecret').value = secret;
+ document.getElementById('webhookModal').classList.add('active');
+}
+
+/**
+ * Delete repository
+ */
+async function deleteRepository(repoId, repoName) {
+ const deleteFiles = confirm(`Repository "${repoName}" aus Konfiguration entfernen?\n\nKlicke OK, um auch die Dateien vom Server zu löschen.\nKlicke Abbrechen, um nur die Konfiguration zu entfernen.`);
+
+ if (deleteFiles === null) return; // User cancelled
+
+ try {
+ const response = await fetch('api/repos.php', {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ id: repoId,
+ delete_files: deleteFiles
+ })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`Repository "${repoName}" wurde entfernt.`, 'success');
+ loadDashboard();
+ } else {
+ showToast(result.error || 'Fehler beim Löschen', 'error');
+ }
+ } catch (error) {
+ console.error('Error deleting repository:', error);
+ showToast('Fehler beim Löschen', 'error');
+ }
+}
+
+/**
+ * Fetch branches from GitHub
+ */
+async function fetchBranches() {
+ const repoUrl = document.getElementById('repoUrl').value;
+
+ if (!repoUrl) return;
+
+ const branchSelect = document.getElementById('repoBranch');
+ const branchLoading = document.getElementById('branchLoading');
+
+ branchLoading.style.display = 'block';
+
+ try {
+ // This would need to be implemented in the backend
+ // For now, keep default branches
+ branchLoading.style.display = 'none';
+ } catch (error) {
+ console.error('Error fetching branches:', error);
+ branchLoading.style.display = 'none';
+ }
+}
+
+/**
+ * Refresh logs
+ */
+function refreshLogs() {
+ loadLogs();
+ showToast('Logs aktualisiert', 'info');
+}
+
+/**
+ * Close modal
+ */
+function closeModal(modalId) {
+ document.getElementById(modalId).classList.remove('active');
+}
+
+/**
+ * Show toast notification
+ */
+function showToast(message, type = 'info') {
+ const container = document.getElementById('toastContainer');
+ const toast = document.createElement('div');
+
+ toast.className = `toast ${type}`;
+ toast.textContent = message;
+
+ container.appendChild(toast);
+
+ // Auto-remove after 5 seconds
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ setTimeout(() => toast.remove(), 300);
+ }, 5000);
+}
+
+/**
+ * Copy text to clipboard
+ */
+function copyToClipboard(elementId) {
+ const element = document.getElementById(elementId);
+ element.select();
+ document.execCommand('copy');
+ showToast('In Zwischenablage kopiert!', 'success');
+}
+
+/**
+ * Helper: Escape HTML
+ */
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+/**
+ * Helper: Get status text
+ */
+function getStatusText(status) {
+ const statusMap = {
+ 'synced': '✅ Synchronisiert',
+ 'cloning': '⏳ Wird geklont...',
+ 'error': '❌ Fehler',
+ 'conflict': '⚠️ Konflikt',
+ 'pending': '⏸️ Ausstehend'
+ };
+
+ return statusMap[status] || status;
+}
+
+/**
+ * Helper: Format log details
+ */
+function formatLogDetails(details) {
+ return Object.entries(details)
+ .map(([key, value]) => `${key}: ${value}`)
+ .join(' | ');
+}
+
+// Close modal when clicking outside
+window.addEventListener('click', function(event) {
+ if (event.target.classList.contains('modal')) {
+ event.target.classList.remove('active');
+ }
+});
+
+// Close modal with Escape key
+window.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape') {
+ document.querySelectorAll('.modal.active').forEach(modal => {
+ modal.classList.remove('active');
+ });
+ }
+});
diff --git a/gitpusher/public/webhook.php b/gitpusher/public/webhook.php
new file mode 100644
index 0000000..2be668b
--- /dev/null
+++ b/gitpusher/public/webhook.php
@@ -0,0 +1,131 @@
+ 'Method not allowed']);
+ exit;
+}
+
+// Get payload
+$payload = file_get_contents('php://input');
+$data = json_decode($payload, true);
+
+if (json_last_error() !== JSON_ERROR_NONE) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Invalid JSON payload']);
+ exit;
+}
+
+// Initialize classes
+$configManager = new ConfigManager();
+$logger = new Logger($configManager);
+$gitHandler = new GitHandler($logger, $configManager);
+
+// Get repository URL from payload
+$repoUrl = $data['repository']['clone_url'] ?? null;
+
+if (!$repoUrl) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Repository URL not found in payload']);
+ exit;
+}
+
+// Find matching repository in config
+$repos = $configManager->getRepositories();
+$matchedRepo = null;
+
+foreach ($repos as $repo) {
+ if ($repo['repo_url'] === $repoUrl) {
+ $matchedRepo = $repo;
+ break;
+ }
+}
+
+if (!$matchedRepo) {
+ http_response_code(404);
+ $logger->warning('webhook', "Webhook received for unknown repository: $repoUrl");
+ echo json_encode(['error' => 'Repository not configured']);
+ exit;
+}
+
+// Verify webhook signature if secret is configured
+$webhookSecret = $configManager->getWebhookSecret($matchedRepo['id']);
+
+if ($webhookSecret) {
+ $signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
+
+ if (empty($signature)) {
+ http_response_code(401);
+ $logger->error($matchedRepo['id'], "Webhook signature missing");
+ echo json_encode(['error' => 'Signature required']);
+ exit;
+ }
+
+ $expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);
+
+ if (!hash_equals($expectedSignature, $signature)) {
+ http_response_code(401);
+ $logger->error($matchedRepo['id'], "Invalid webhook signature");
+ echo json_encode(['error' => 'Invalid signature']);
+ exit;
+ }
+}
+
+// Check if the push is for the configured branch
+$ref = $data['ref'] ?? '';
+$pushedBranch = str_replace('refs/heads/', '', $ref);
+
+if ($pushedBranch !== $matchedRepo['branch']) {
+ $logger->info($matchedRepo['id'], "Ignoring push to branch '$pushedBranch' (configured: '{$matchedRepo['branch']}')");
+ echo json_encode([
+ 'success' => true,
+ 'message' => 'Ignored - different branch',
+ 'pushed_branch' => $pushedBranch,
+ 'configured_branch' => $matchedRepo['branch']
+ ]);
+ exit;
+}
+
+// Log webhook received
+$commits = $data['commits'] ?? [];
+$logger->info($matchedRepo['id'], "Webhook received: " . count($commits) . " commits pushed", [
+ 'pusher' => $data['pusher']['name'] ?? 'unknown',
+ 'branch' => $pushedBranch
+]);
+
+// Perform git pull
+$result = $gitHandler->pull(
+ $matchedRepo['id'],
+ $matchedRepo['target_path'],
+ $matchedRepo['branch']
+);
+
+// Return result
+if ($result['success']) {
+ http_response_code(200);
+ echo json_encode([
+ 'success' => true,
+ 'message' => $result['message'],
+ 'files_changed' => $result['files_changed'] ?? 0
+ ]);
+} else {
+ // Still return 200 to GitHub, but log the error
+ http_response_code(200);
+ echo json_encode([
+ 'success' => false,
+ 'message' => $result['message'],
+ 'conflict' => $result['conflict'] ?? false
+ ]);
+}
diff --git a/gitpusher/src/.htaccess b/gitpusher/src/.htaccess
new file mode 100644
index 0000000..9694c0e
--- /dev/null
+++ b/gitpusher/src/.htaccess
@@ -0,0 +1,8 @@
+# Deny all access to source directory
+Require all denied
+
+# Alternative for older Apache versions
+
+ Order deny,allow
+ Deny from all
+
diff --git a/gitpusher/src/ConfigManager.php b/gitpusher/src/ConfigManager.php
new file mode 100644
index 0000000..850c095
--- /dev/null
+++ b/gitpusher/src/ConfigManager.php
@@ -0,0 +1,199 @@
+dataDir = $dataDir;
+ $this->ensureDataDirExists();
+ }
+
+ /**
+ * Ensure data directory exists with proper permissions
+ */
+ private function ensureDataDirExists() {
+ if (!file_exists($this->dataDir)) {
+ mkdir($this->dataDir, 0755, true);
+ }
+ }
+
+ /**
+ * Read JSON file
+ */
+ public function read($filename) {
+ $filepath = $this->dataDir . '/' . $filename;
+
+ if (!file_exists($filepath)) {
+ return [];
+ }
+
+ $content = file_get_contents($filepath);
+ $data = json_decode($content, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ error_log("JSON decode error in $filename: " . json_last_error_msg());
+ return [];
+ }
+
+ return $data;
+ }
+
+ /**
+ * Write JSON file
+ */
+ public function write($filename, $data) {
+ $filepath = $this->dataDir . '/' . $filename;
+
+ $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+
+ if ($json === false) {
+ error_log("JSON encode error: " . json_last_error_msg());
+ return false;
+ }
+
+ $result = file_put_contents($filepath, $json);
+
+ if ($result === false) {
+ error_log("Failed to write file: $filepath");
+ return false;
+ }
+
+ // Set appropriate permissions (readable only by owner)
+ chmod($filepath, 0600);
+
+ return true;
+ }
+
+ /**
+ * Get all repositories from config
+ */
+ public function getRepositories() {
+ $config = $this->read('config.json');
+ return $config['repositories'] ?? [];
+ }
+
+ /**
+ * Get repository by ID
+ */
+ public function getRepository($repoId) {
+ $repos = $this->getRepositories();
+
+ foreach ($repos as $repo) {
+ if ($repo['id'] === $repoId) {
+ return $repo;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Add new repository
+ */
+ public function addRepository($repoData) {
+ $config = $this->read('config.json');
+
+ if (!isset($config['repositories'])) {
+ $config['repositories'] = [];
+ }
+
+ // Generate unique ID
+ $repoData['id'] = uniqid('repo_', true);
+ $repoData['created_at'] = date('Y-m-d H:i:s');
+ $repoData['status'] = 'pending';
+
+ $config['repositories'][] = $repoData;
+
+ return $this->write('config.json', $config) ? $repoData['id'] : false;
+ }
+
+ /**
+ * Update repository
+ */
+ public function updateRepository($repoId, $updates) {
+ $config = $this->read('config.json');
+
+ if (!isset($config['repositories'])) {
+ return false;
+ }
+
+ foreach ($config['repositories'] as &$repo) {
+ if ($repo['id'] === $repoId) {
+ $repo = array_merge($repo, $updates);
+ $repo['updated_at'] = date('Y-m-d H:i:s');
+ return $this->write('config.json', $config);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete repository
+ */
+ public function deleteRepository($repoId) {
+ $config = $this->read('config.json');
+
+ if (!isset($config['repositories'])) {
+ return false;
+ }
+
+ $config['repositories'] = array_filter($config['repositories'], function($repo) use ($repoId) {
+ return $repo['id'] !== $repoId;
+ });
+
+ // Re-index array
+ $config['repositories'] = array_values($config['repositories']);
+
+ return $this->write('config.json', $config);
+ }
+
+ /**
+ * Get GitHub Personal Access Token
+ */
+ public function getGitHubToken() {
+ $secrets = $this->read('secrets.json');
+ return $secrets['github_pat'] ?? null;
+ }
+
+ /**
+ * Set GitHub Personal Access Token
+ */
+ public function setGitHubToken($token) {
+ $secrets = $this->read('secrets.json');
+ $secrets['github_pat'] = $token;
+ return $this->write('secrets.json', $secrets);
+ }
+
+ /**
+ * Get webhook secret for a repository
+ */
+ public function getWebhookSecret($repoId) {
+ $secrets = $this->read('secrets.json');
+ return $secrets['webhook_secrets'][$repoId] ?? null;
+ }
+
+ /**
+ * Set webhook secret for a repository
+ */
+ public function setWebhookSecret($repoId, $secret) {
+ $secrets = $this->read('secrets.json');
+
+ if (!isset($secrets['webhook_secrets'])) {
+ $secrets['webhook_secrets'] = [];
+ }
+
+ $secrets['webhook_secrets'][$repoId] = $secret;
+
+ return $this->write('secrets.json', $secrets);
+ }
+
+ /**
+ * Generate secure webhook secret
+ */
+ public function generateWebhookSecret() {
+ return bin2hex(random_bytes(32));
+ }
+}
diff --git a/gitpusher/src/GitHandler.php b/gitpusher/src/GitHandler.php
new file mode 100644
index 0000000..e0d7277
--- /dev/null
+++ b/gitpusher/src/GitHandler.php
@@ -0,0 +1,399 @@
+logger = $logger;
+ $this->configManager = $configManager;
+ }
+
+ /**
+ * Execute shell command and return result
+ */
+ private function exec($command, $cwd = null) {
+ $descriptorspec = [
+ 0 => ['pipe', 'r'], // stdin
+ 1 => ['pipe', 'w'], // stdout
+ 2 => ['pipe', 'w'] // stderr
+ ];
+
+ $process = proc_open($command, $descriptorspec, $pipes, $cwd);
+
+ if (!is_resource($process)) {
+ return [
+ 'success' => false,
+ 'output' => '',
+ 'error' => 'Failed to execute command',
+ 'exit_code' => -1
+ ];
+ }
+
+ $stdout = stream_get_contents($pipes[1]);
+ $stderr = stream_get_contents($pipes[2]);
+
+ fclose($pipes[0]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+
+ $exitCode = proc_close($process);
+
+ return [
+ 'success' => $exitCode === 0,
+ 'output' => trim($stdout),
+ 'error' => trim($stderr),
+ 'exit_code' => $exitCode
+ ];
+ }
+
+ /**
+ * Build Git URL with authentication token
+ */
+ private function buildGitUrl($repoUrl, $token) {
+ // Convert https://github.com/user/repo.git to https://TOKEN@github.com/user/repo.git
+ $parsed = parse_url($repoUrl);
+
+ if (!$parsed || !isset($parsed['host'])) {
+ return $repoUrl;
+ }
+
+ $scheme = $parsed['scheme'] ?? 'https';
+ $host = $parsed['host'];
+ $path = $parsed['path'] ?? '';
+
+ return "$scheme://$token@$host$path";
+ }
+
+ /**
+ * Clone repository
+ */
+ public function cloneRepository($repoId, $repoUrl, $targetPath, $branch = 'main') {
+ $this->logger->info($repoId, "Starting clone of repository to $targetPath");
+
+ // Check if target path already exists
+ if (file_exists($targetPath)) {
+ $this->logger->error($repoId, "Target path already exists: $targetPath");
+ return [
+ 'success' => false,
+ 'message' => 'Target path already exists'
+ ];
+ }
+
+ // Create parent directory if it doesn't exist
+ $parentDir = dirname($targetPath);
+ if (!file_exists($parentDir)) {
+ mkdir($parentDir, 0755, true);
+ }
+
+ // Get GitHub token
+ $token = $this->configManager->getGitHubToken();
+ $gitUrl = $token ? $this->buildGitUrl($repoUrl, $token) : $repoUrl;
+
+ // Clone repository
+ $command = sprintf(
+ 'git clone --branch %s %s %s 2>&1',
+ escapeshellarg($branch),
+ escapeshellarg($gitUrl),
+ escapeshellarg($targetPath)
+ );
+
+ $result = $this->exec($command);
+
+ if ($result['success']) {
+ $this->logger->success($repoId, "Repository cloned successfully", [
+ 'path' => $targetPath,
+ 'branch' => $branch
+ ]);
+
+ return [
+ 'success' => true,
+ 'message' => 'Repository cloned successfully',
+ 'output' => $result['output']
+ ];
+ } else {
+ $this->logger->error($repoId, "Failed to clone repository", [
+ 'error' => $result['error'],
+ 'output' => $result['output']
+ ]);
+
+ return [
+ 'success' => false,
+ 'message' => 'Failed to clone repository',
+ 'error' => $result['error']
+ ];
+ }
+ }
+
+ /**
+ * Pull latest changes
+ */
+ public function pull($repoId, $targetPath, $branch = 'main') {
+ $this->logger->info($repoId, "Starting pull for $targetPath");
+
+ // Check if path exists
+ if (!file_exists($targetPath)) {
+ $this->logger->error($repoId, "Repository path does not exist: $targetPath");
+ return [
+ 'success' => false,
+ 'message' => 'Repository path does not exist'
+ ];
+ }
+
+ // Check if it's a git repository
+ if (!file_exists("$targetPath/.git")) {
+ $this->logger->error($repoId, "Not a git repository: $targetPath");
+ return [
+ 'success' => false,
+ 'message' => 'Not a git repository'
+ ];
+ }
+
+ // Get current commit before pull
+ $currentCommit = $this->getCurrentCommit($targetPath);
+
+ // Pull changes
+ $command = sprintf(
+ 'cd %s && git pull origin %s 2>&1',
+ escapeshellarg($targetPath),
+ escapeshellarg($branch)
+ );
+
+ $result = $this->exec($command);
+
+ // Check for merge conflicts
+ if (!$result['success'] || strpos($result['output'], 'CONFLICT') !== false) {
+ $this->logger->warning($repoId, "Merge conflict detected", [
+ 'output' => $result['output'],
+ 'error' => $result['error']
+ ]);
+
+ $this->configManager->updateRepository($repoId, ['status' => 'conflict']);
+
+ return [
+ 'success' => false,
+ 'message' => 'Merge conflict detected',
+ 'conflict' => true,
+ 'output' => $result['output']
+ ];
+ }
+
+ // Get new commit after pull
+ $newCommit = $this->getCurrentCommit($targetPath);
+
+ // Count changed files
+ $changedFiles = $this->getChangedFilesBetweenCommits($targetPath, $currentCommit, $newCommit);
+
+ if ($result['success']) {
+ $this->logger->success($repoId, "Pull completed successfully", [
+ 'files_changed' => count($changedFiles),
+ 'old_commit' => substr($currentCommit, 0, 7),
+ 'new_commit' => substr($newCommit, 0, 7)
+ ]);
+
+ $this->configManager->updateRepository($repoId, [
+ 'status' => 'synced',
+ 'last_sync' => date('Y-m-d H:i:s'),
+ 'last_commit' => $newCommit
+ ]);
+
+ return [
+ 'success' => true,
+ 'message' => 'Pull completed successfully',
+ 'files_changed' => count($changedFiles),
+ 'output' => $result['output']
+ ];
+ } else {
+ $this->logger->error($repoId, "Pull failed", [
+ 'error' => $result['error'],
+ 'output' => $result['output']
+ ]);
+
+ return [
+ 'success' => false,
+ 'message' => 'Pull failed',
+ 'error' => $result['error']
+ ];
+ }
+ }
+
+ /**
+ * Revert to specific commit
+ */
+ public function revert($repoId, $targetPath, $commitHash) {
+ $this->logger->info($repoId, "Starting revert to commit $commitHash");
+
+ if (!file_exists($targetPath)) {
+ return [
+ 'success' => false,
+ 'message' => 'Repository path does not exist'
+ ];
+ }
+
+ // Create revert commit
+ $command = sprintf(
+ 'cd %s && git revert --no-edit %s 2>&1',
+ escapeshellarg($targetPath),
+ escapeshellarg($commitHash)
+ );
+
+ $result = $this->exec($command);
+
+ if ($result['success']) {
+ $this->logger->success($repoId, "Reverted to commit $commitHash", [
+ 'commit' => $commitHash
+ ]);
+
+ $newCommit = $this->getCurrentCommit($targetPath);
+
+ $this->configManager->updateRepository($repoId, [
+ 'last_commit' => $newCommit,
+ 'last_sync' => date('Y-m-d H:i:s')
+ ]);
+
+ return [
+ 'success' => true,
+ 'message' => 'Revert completed successfully',
+ 'output' => $result['output']
+ ];
+ } else {
+ $this->logger->error($repoId, "Revert failed", [
+ 'error' => $result['error']
+ ]);
+
+ return [
+ 'success' => false,
+ 'message' => 'Revert failed',
+ 'error' => $result['error']
+ ];
+ }
+ }
+
+ /**
+ * Get current commit hash
+ */
+ public function getCurrentCommit($targetPath) {
+ $result = $this->exec("cd " . escapeshellarg($targetPath) . " && git rev-parse HEAD 2>&1");
+ return $result['success'] ? trim($result['output']) : null;
+ }
+
+ /**
+ * Get commit history
+ */
+ public function getCommitHistory($targetPath, $limit = 20) {
+ $command = sprintf(
+ 'cd %s && git log --pretty=format:"%%H|%%an|%%ae|%%at|%%s" -n %d 2>&1',
+ escapeshellarg($targetPath),
+ (int)$limit
+ );
+
+ $result = $this->exec($command);
+
+ if (!$result['success']) {
+ return [];
+ }
+
+ $commits = [];
+ $lines = explode("\n", $result['output']);
+
+ foreach ($lines as $line) {
+ if (empty($line)) continue;
+
+ $parts = explode('|', $line);
+ if (count($parts) !== 5) continue;
+
+ $commits[] = [
+ 'hash' => $parts[0],
+ 'hash_short' => substr($parts[0], 0, 7),
+ 'author_name' => $parts[1],
+ 'author_email' => $parts[2],
+ 'timestamp' => date('Y-m-d H:i:s', (int)$parts[3]),
+ 'message' => $parts[4]
+ ];
+ }
+
+ return $commits;
+ }
+
+ /**
+ * Get changed files between commits
+ */
+ private function getChangedFilesBetweenCommits($targetPath, $oldCommit, $newCommit) {
+ if ($oldCommit === $newCommit) {
+ return [];
+ }
+
+ $command = sprintf(
+ 'cd %s && git diff --name-only %s %s 2>&1',
+ escapeshellarg($targetPath),
+ escapeshellarg($oldCommit),
+ escapeshellarg($newCommit)
+ );
+
+ $result = $this->exec($command);
+
+ if (!$result['success']) {
+ return [];
+ }
+
+ return array_filter(explode("\n", $result['output']));
+ }
+
+ /**
+ * Get repository status
+ */
+ public function getStatus($targetPath) {
+ $command = sprintf(
+ 'cd %s && git status --porcelain 2>&1',
+ escapeshellarg($targetPath)
+ );
+
+ $result = $this->exec($command);
+
+ return [
+ 'success' => $result['success'],
+ 'clean' => $result['success'] && empty($result['output']),
+ 'output' => $result['output']
+ ];
+ }
+
+ /**
+ * Get current branch
+ */
+ public function getCurrentBranch($targetPath) {
+ $result = $this->exec("cd " . escapeshellarg($targetPath) . " && git branch --show-current 2>&1");
+ return $result['success'] ? trim($result['output']) : null;
+ }
+
+ /**
+ * Fetch available branches from remote
+ */
+ public function getRemoteBranches($repoUrl) {
+ $token = $this->configManager->getGitHubToken();
+ $gitUrl = $token ? $this->buildGitUrl($repoUrl, $token) : $repoUrl;
+
+ $command = sprintf(
+ 'git ls-remote --heads %s 2>&1',
+ escapeshellarg($gitUrl)
+ );
+
+ $result = $this->exec($command);
+
+ if (!$result['success']) {
+ return [];
+ }
+
+ $branches = [];
+ $lines = explode("\n", $result['output']);
+
+ foreach ($lines as $line) {
+ if (preg_match('/refs\/heads\/(.+)$/', $line, $matches)) {
+ $branches[] = $matches[1];
+ }
+ }
+
+ return $branches;
+ }
+}
diff --git a/gitpusher/src/Logger.php b/gitpusher/src/Logger.php
new file mode 100644
index 0000000..a002406
--- /dev/null
+++ b/gitpusher/src/Logger.php
@@ -0,0 +1,166 @@
+configManager = $configManager;
+ }
+
+ /**
+ * Add log entry
+ */
+ public function log($repoId, $type, $message, $details = []) {
+ $logs = $this->configManager->read('log.json');
+
+ if (!isset($logs['entries'])) {
+ $logs['entries'] = [];
+ }
+
+ $entry = [
+ 'id' => uniqid('log_', true),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'repo_id' => $repoId,
+ 'type' => $type, // success, error, warning, info
+ 'message' => $message,
+ 'details' => $details
+ ];
+
+ // Add to beginning of array
+ array_unshift($logs['entries'], $entry);
+
+ // Keep only last N entries
+ if (count($logs['entries']) > $this->maxLogEntries) {
+ $logs['entries'] = array_slice($logs['entries'], 0, $this->maxLogEntries);
+ }
+
+ $this->configManager->write('log.json', $logs);
+
+ // Also log to PHP error log for debugging
+ error_log("[GitPusher] [$type] $repoId: $message");
+
+ return $entry['id'];
+ }
+
+ /**
+ * Log success
+ */
+ public function success($repoId, $message, $details = []) {
+ return $this->log($repoId, 'success', $message, $details);
+ }
+
+ /**
+ * Log error
+ */
+ public function error($repoId, $message, $details = []) {
+ return $this->log($repoId, 'error', $message, $details);
+ }
+
+ /**
+ * Log warning
+ */
+ public function warning($repoId, $message, $details = []) {
+ return $this->log($repoId, 'warning', $message, $details);
+ }
+
+ /**
+ * Log info
+ */
+ public function info($repoId, $message, $details = []) {
+ return $this->log($repoId, 'info', $message, $details);
+ }
+
+ /**
+ * Get all log entries
+ */
+ public function getAll($limit = 100, $offset = 0) {
+ $logs = $this->configManager->read('log.json');
+ $entries = $logs['entries'] ?? [];
+
+ return array_slice($entries, $offset, $limit);
+ }
+
+ /**
+ * Get logs for specific repository
+ */
+ public function getByRepository($repoId, $limit = 100) {
+ $logs = $this->configManager->read('log.json');
+ $entries = $logs['entries'] ?? [];
+
+ $filtered = array_filter($entries, function($entry) use ($repoId) {
+ return $entry['repo_id'] === $repoId;
+ });
+
+ return array_slice(array_values($filtered), 0, $limit);
+ }
+
+ /**
+ * Get logs by type
+ */
+ public function getByType($type, $limit = 100) {
+ $logs = $this->configManager->read('log.json');
+ $entries = $logs['entries'] ?? [];
+
+ $filtered = array_filter($entries, function($entry) use ($type) {
+ return $entry['type'] === $type;
+ });
+
+ return array_slice(array_values($filtered), 0, $limit);
+ }
+
+ /**
+ * Clear all logs
+ */
+ public function clear() {
+ return $this->configManager->write('log.json', ['entries' => []]);
+ }
+
+ /**
+ * Clear logs for specific repository
+ */
+ public function clearByRepository($repoId) {
+ $logs = $this->configManager->read('log.json');
+ $entries = $logs['entries'] ?? [];
+
+ $filtered = array_filter($entries, function($entry) use ($repoId) {
+ return $entry['repo_id'] !== $repoId;
+ });
+
+ $logs['entries'] = array_values($filtered);
+
+ return $this->configManager->write('log.json', $logs);
+ }
+
+ /**
+ * Get statistics
+ */
+ public function getStats() {
+ $logs = $this->configManager->read('log.json');
+ $entries = $logs['entries'] ?? [];
+
+ $stats = [
+ 'total' => count($entries),
+ 'success' => 0,
+ 'error' => 0,
+ 'warning' => 0,
+ 'info' => 0,
+ 'last_24h' => 0
+ ];
+
+ $yesterday = strtotime('-24 hours');
+
+ foreach ($entries as $entry) {
+ $stats[$entry['type']]++;
+
+ $timestamp = strtotime($entry['timestamp']);
+ if ($timestamp >= $yesterday) {
+ $stats['last_24h']++;
+ }
+ }
+
+ return $stats;
+ }
+}