This commit is contained in:
ilkeral
2025-08-08 07:24:25 +03:00
parent 342f1314c7
commit f4ee7a2d0b
29 changed files with 5189 additions and 1140 deletions

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

2
.idea/webServers.xml generated
View File

@ -3,7 +3,7 @@
<component name="WebServers">
<option name="servers">
<webServer id="229148d6-cc65-4e83-9311-3d59c0d43b95" name="YONETIM">
<fileTransfer rootFolder="/srv/yonetim" accessType="SFTP" host="51.38.103.237" port="22" sshConfigId="076ef7a5-08c2-4382-acef-942ea7decf61" sshConfig="root@51.38.103.237:22 password">
<fileTransfer rootFolder="/srv/ekolshop" accessType="SFTP" host="192.248.177.29" port="22" sshConfigId="161be5a0-a6ab-41bb-90fb-0c778aa5755d" sshConfig="root@192.248.177.29:22 password">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" passiveMode="true" shareSSLContext="true" />
</advancedOptions>

View File

@ -1,56 +1,45 @@
#!/bin/bash
set -e
echo "=== Container başlatılıyor ==="
echo "Container başlatılıyor, izinler kontrol ediliyor..."
# Veritabanı dosyası yolu
DB_FILE="/app/db.sqlite3"
DB_DIR="/app"
echo "Mevcut durumu kontrol ediyorum..."
ls -la /app/ || true
# Veritabanı dosyası kontrolü ve oluşturma
if [ ! -f "$DB_FILE" ]; then
echo "SQLite veritabanı bulunamadı, oluşturuluyor..."
touch "$DB_FILE"
# Root olarak çalışıyoruz (user değiştirmeden önce)
# SQLite veritabanı izinlerini düzelt
if [ -f /app/db.sqlite3 ]; then
echo "SQLite veritabanı bulundu, izinler düzeltiliyor..."
chown 1000:1000 /app/db.sqlite3
chmod 664 /app/db.sqlite3
echo "Veritabanı izinleri düzeltildi."
else
echo "SQLite veritabanı bulunamadı, yeni oluşturulacak..."
touch /app/db.sqlite3
chown 1000:1000 /app/db.sqlite3
chmod 664 /app/db.sqlite3
fi
# Tüm /app dizinini 1000:1000 kullanıcısına ata
echo "Dizin sahipliği ayarlanıyor..."
# App dizini izinlerini kontrol et
chown -R 1000:1000 /app
chmod 775 /app
# Veritabanı dosyası için özel izinler
echo "Veritabanı izinleri ayarlanıyor..."
chmod 666 "$DB_FILE" # Daha geniş izin
chmod 777 "$DB_DIR" # Dizin için tam izin
# Auto backup scriptini kopyala ve izinleri ayarla
echo "Auto backup script kurulumu yapılıyor..."
if [ -f /app/build/auto_backup.sh ]; then
cp /app/build/auto_backup.sh /usr/local/bin/auto_backup.sh
chmod +x /usr/local/bin/auto_backup.sh
echo "Auto backup script kurulumu tamamlandı."
else
echo "UYARI: Auto backup script bulunamadı!"
fi
# Gerekli dizinleri oluştur
mkdir -p /app/media /app/static /app/logs
mkdir -p /tmp/backups
# Gerekli dizinleri oluştur ve izinleri ayarla
mkdir -p /app/media /app/static /app/logs /tmp/backups
chown -R 1000:1000 /app/media /app/static /app/logs /tmp/backups
chmod -R 777 /app/media /app/static /app/logs /tmp/backups
chmod -R 755 /app/media /app/static /app/logs /tmp/backups
# /tmp dizinine de tam izin ver
chmod 777 /tmp
chown 1000:1000 /tmp
# /tmp dizini izinleri
chown -R 1000:1000 /tmp/backups
chmod -R 755 /tmp/backups
echo "Final izin kontrolü:"
ls -la /app/db.sqlite3 || true
ls -ld /app/ || true
echo "İzinler ayarlandı."
echo "=== İzinler ayarlandı, appuser olarak geçiliyor ==="
# Django migrate çalıştır (root olarak)
echo "Django migrate çalıştırılıyor..."
cd /app
python manage.py migrate --noinput || echo "Migrate hatası, devam ediliyor..."
# Veritabanı izinlerini tekrar ayarla
chown 1000:1000 "$DB_FILE"
chmod 666 "$DB_FILE"
echo "=== Uygulama başlatılıyor ==="
# appuser olarak uygulamayı başlat
# appuser olarak geç ve komutu çalıştır
exec su-exec 1000:1000 "$@"

Binary file not shown.

View File

@ -7,7 +7,10 @@ services:
command: gunicorn --bind 0.0.0.0:8000 yonetim.wsgi:application
volumes:
- ./:/app:rw
- /tmp:/tmp:rw
- ./db.sqlite3:/app/db.sqlite3:rw
# Geçici dizin yapılandırması - Yedekleme için önemli
tmpfs:
- /tmp:exec,mode=1777,size=2g
#ports:
# - 8025:8000
networks:

Binary file not shown.

View File

@ -1,5 +1,40 @@
from django.contrib import admin
from .models import SSHCredential, Project, SSHLog
from .models import SSHCredential, Project, SSHLog, Customer, Invoice, InvoiceItem
from .system_settings import SystemSettings
@admin.register(SystemSettings)
class SystemSettingsAdmin(admin.ModelAdmin):
list_display = ('__str__', 'backup_enabled', 'backup_frequency', 'updated_at')
fieldsets = (
('Yedekleme Ayarları', {
'fields': ('backup_enabled', 'backup_frequency', 'backup_hour', 'backup_minute',
'backup_day_of_week', 'backup_day_of_month', 'backup_retention_days',
'backup_compression', 'backup_format', 'backup_directory')
}),
('S3 Depolama', {
'fields': ('use_s3_storage', 's3_access_key', 's3_secret_key',
's3_bucket_name', 's3_region')
}),
('Bildirim Ayarları', {
'fields': ('email_notifications', 'notification_email')
}),
('SSH Ayarları', {
'fields': ('ssh_key_path', 'ssh_key_passphrase')
}),
('Crontab Bilgisi', {
'fields': ('backup_crontab_expression', 'updated_at'),
'classes': ('collapse',)
})
)
readonly_fields = ('backup_crontab_expression', 'updated_at')
def has_add_permission(self, request):
# Sadece bir kayıt olabilir
return SystemSettings.objects.count() == 0
def has_delete_permission(self, request, obj=None):
# Silme işlemi engellenir
return False
@admin.register(SSHCredential)
class SSHCredentialAdmin(admin.ModelAdmin):
@ -69,3 +104,53 @@ class SSHLogAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
return False # Log kayıtları değiştirilemez
class InvoiceItemInline(admin.TabularInline):
model = InvoiceItem
extra = 1
fields = ('project', 'description', 'amount')
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
form = formset.form
form.base_fields['description'].widget.attrs['style'] = 'width: 300px'
return formset
@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
list_display = ('invoice_number', 'customer', 'issue_date', 'due_date', 'status', 'total_amount')
list_filter = ('status', 'issue_date', 'payment_method')
search_fields = ('invoice_number', 'customer__name', 'customer__company_name')
readonly_fields = ('invoice_number', 'total_amount', 'created_at', 'updated_at')
inlines = [InvoiceItemInline]
date_hierarchy = 'issue_date'
fieldsets = (
('Fatura Bilgileri', {
'fields': ('invoice_number', 'customer', 'issue_date', 'due_date', 'status', 'payment_method')
}),
('Tutar Bilgileri', {
'fields': ('total_amount',)
}),
('Ek Bilgiler', {
'fields': ('notes', 'payment_notes'),
'classes': ('collapse',)
}),
('Sistem Bilgileri', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
})
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('customer')
@admin.register(InvoiceItem)
class InvoiceItemAdmin(admin.ModelAdmin):
list_display = ('invoice', 'description', 'project', 'amount')
list_filter = ('invoice__status',)
search_fields = ('description', 'invoice__invoice_number', 'project__name')
def get_queryset(self, request):
return super().get_queryset(request).select_related('invoice', 'project')

View File

@ -1,128 +1,31 @@
import os
import io
import sys
import locale
import zipfile
import boto3
import tempfile
import traceback
from boto3.s3.transfer import TransferConfig
from django.utils.text import slugify
from datetime import datetime
import requests
import stat
# Add urllib3 import to disable SSL warnings
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
haric_dosya_uzantilari = ['.zip', ]
excluded_folders = ['venv', 'yedek', '.idea', '.sock']
hostname = "ams1.vultrobjects.com"
secret_key = "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef"
access_key = "KQAOMJ8CQ8HP4CY23YPK"
excluded_folders = ['venv', 'yedek', '.idea', '.sock', '.venv']
x = 1
def upload_file_via_presigned_url(url, file_path):
if not os.path.exists(file_path):
print(f"Dosya bulunamadi: {file_path}")
return False
with open(file_path, 'rb') as file_data:
try:
response = requests.put(url, data=file_data)
if response.status_code == 200:
print("Dosya yuklendi!")
return True
else:
print(f"Yukleme olmadi. Status code: {response.status_code}")
print(f"Response: {response.content}")
return False
except Exception as e:
print(f"Yukleme hatasi: {e}")
return False
def get_filtered_folder_names(directory, excluded_folders):
folder_names = []
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if os.path.isdir(item_path) and item not in excluded_folders:
folder_names.append(item)
return folder_names
def zip_klasor(ziplenecek_klasor, hedef_zip_adi, haric_klasorler=[], haric_dosya_uzantilari=[]):
# Parametrelerin geçerliliğini kontrol et
if not ziplenecek_klasor or not hedef_zip_adi:
raise ValueError("Ziplenecek klasör ve hedef zip adı boş olamaz")
if not os.path.exists(ziplenecek_klasor):
raise FileNotFoundError(f"Ziplenecek klasör bulunamadı: {ziplenecek_klasor}")
# Hedef zip dosyasının bulunacağı dizini oluştur ve izinleri ayarla
hedef_dizin = os.path.dirname(hedef_zip_adi)
# Eğer hedef dizin boşsa, mevcut dizini kullan
if not hedef_dizin:
hedef_dizin = "."
hedef_zip_adi = os.path.join(hedef_dizin, hedef_zip_adi)
if not os.path.exists(hedef_dizin):
os.makedirs(hedef_dizin, mode=0o755, exist_ok=True)
# Zip dosyası oluşturmadan önce izinleri kontrol et
if os.path.exists(hedef_zip_adi):
try:
os.chmod(hedef_zip_adi, 0o666)
except Exception as e:
print(f"Mevcut zip dosyasinin izinleri guncellenemedi: {e}")
with zipfile.ZipFile(hedef_zip_adi, 'w', zipfile.ZIP_DEFLATED) as zipf:
for klasor_yolu, _, dosya_listesi in os.walk(ziplenecek_klasor):
if not any(k in klasor_yolu for k in haric_klasorler):
for dosya in dosya_listesi:
dosya_adi, dosya_uzantisi = os.path.splitext(dosya)
dosya_yolu = os.path.join(klasor_yolu, dosya)
# Dosyanın var olup olmadığını kontrol et
if not os.path.exists(dosya_yolu):
print(f"Dosya bulunamadi: {dosya_yolu}")
continue
# Socket dosyalarını atla
try:
file_stat = os.stat(dosya_yolu)
if stat.S_ISSOCK(file_stat.st_mode):
print(f"Socket dosyasi atlandi: {dosya_yolu}")
continue
except (OSError, PermissionError) as e:
print(f"Dosya stat alinamadi: {dosya_yolu} -> Hata: {e}")
continue
if dosya_uzantisi.lower() not in haric_dosya_uzantilari:
try:
# Dosya okuma izinlerini kontrol et
if os.access(dosya_yolu, os.R_OK):
zipf.write(dosya_yolu, os.path.relpath(dosya_yolu, ziplenecek_klasor))
print(f"Dosya eklendi: {dosya_yolu}")
else:
print(f"Dosya okuma izni yok: {dosya_yolu}")
except (PermissionError, OSError) as e:
print(f"Dosya eklenemedi: {dosya_yolu} -> Hata: {e}")
except Exception as e:
print(f"Beklenmeyen hata: {dosya_yolu} -> Hata: {e}")
# Oluşturulan zip dosyasının izinlerini ayarla
try:
os.chmod(hedef_zip_adi, 0o644)
print(f"Zip dosyasi olusturuldu: {hedef_zip_adi}")
except Exception as e:
print(f"Zip dosyasi izinleri ayarlanamadi: {e}")
def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], excluded_extensions=[]):
"""SSH üzerinden uzak sunucuda zip dosyası oluşturur"""
# Uzak sunucuda geçici zip dosyası yolu
remote_zip_path = f"/tmp/{zip_name}"
# Önce kaynak dizinin varlığını kontrol et
check_dir_command = f"test -d '{source_dir}' && echo 'exists' || echo 'not_exists'"
try:
stdout, stderr, status = ssh_manager.execute_command(check_dir_command)
@ -131,8 +34,15 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
except Exception as e:
raise Exception(f"Dizin kontrolü hatası: {str(e)}")
# Zip komutunun varlığını kontrol et ve gerekirse kur
zip_check_command = "which zip || command -v zip"
# Encoding değişkenini kontrol et
locale_command = "locale -a | grep -i utf"
stdout, stderr, status = ssh_manager.execute_command(locale_command)
print(f"Sunucudaki UTF-8 locale'lar: {stdout}")
# LC_ALL ve LANG değişkenlerini UTF-8 olarak ayarla
env_setup = "export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=en_US.UTF-8 2>/dev/null || export LC_CTYPE=UTF-8; export LANG=C.UTF-8 2>/dev/null || export LANG=en_US.UTF-8;"
zip_check_command = f"{env_setup} which zip || command -v zip"
try:
stdout, stderr, status = ssh_manager.execute_command(zip_check_command)
if not status:
@ -142,20 +52,23 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
except Exception as e:
raise Exception(f"Zip komutu kontrolü hatası: {str(e)}")
# Hariç tutulacak klasörler için exclude parametresi
base_path = os.path.dirname(source_dir)
folder_to_zip = os.path.basename(source_dir)
exclude_args = ""
for folder in excluded_folders:
exclude_args += f" --exclude='{folder}/*' --exclude='{folder}'"
# zip'in exclude path'i, zip komutunun çalıştığı dizine göre olmalı.
# cd '{base_path}' yaptığımız için, exclude path'i '{folder_to_zip}/{folder}/*' şeklinde olmalı.
exclude_args += f" -x '{folder_to_zip}/{folder}/*'"
for ext in excluded_extensions:
exclude_args += f" --exclude='*{ext}'"
exclude_args += f" -x '*{ext}'"
# Eski zip dosyasını temizle
cleanup_command = f"rm -f '{remote_zip_path}'"
ssh_manager.execute_command(cleanup_command)
# Zip komutunu oluştur (daha basit ve güvenilir)
zip_command = f"cd '{source_dir}' && zip -r '{remote_zip_path}' . {exclude_args}"
# UTF-8 desteği için -UN=UTF8 parametresi eklendi ve LC_ALL/LANG değişkenleri ayarlandı
zip_command = f"{env_setup} cd '{base_path}' && zip -UN=UTF8 -r '{remote_zip_path}' '{folder_to_zip}' {exclude_args}"
print(f"Çalıştırılan komut: {zip_command}")
@ -164,10 +77,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
print(f"Zip komutu sonucu - Status: {status}, Stdout: {stdout}, Stderr: {stderr}")
# Zip komutu bazen uyarılarla birlikte başarılı olabilir
# Bu yüzden sadece status kontrolü yerine dosya varlığını da kontrol edelim
# Zip dosyasının varlığını kontrol et
check_command = f"test -f '{remote_zip_path}' && echo 'exists' || echo 'not_exists'"
stdout_check, stderr_check, status_check = ssh_manager.execute_command(check_command)
@ -175,7 +84,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
error_details = f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}"
raise Exception(f"Zip dosyası oluşturulamadı. Detaylar: {error_details}")
# Dosya boyutunu al
size_command = f"stat -c%s '{remote_zip_path}' 2>/dev/null || stat -f%z '{remote_zip_path}' 2>/dev/null || wc -c < '{remote_zip_path}'"
stdout_size, stderr_size, status_size = ssh_manager.execute_command(size_command)
@ -183,7 +91,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
if status_size and stdout_size.strip().isdigit():
file_size = int(stdout_size.strip())
else:
# Boyut alınamazsa alternatif yöntem
ls_command = f"ls -la '{remote_zip_path}'"
stdout_ls, stderr_ls, status_ls = ssh_manager.execute_command(ls_command)
if status_ls:
@ -193,7 +100,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
return remote_zip_path, file_size
except Exception as e:
# Hata durumunda oluşmuş olabilecek zip dosyasını temizle
cleanup_command = f"rm -f '{remote_zip_path}'"
ssh_manager.execute_command(cleanup_command)
raise e
@ -204,23 +110,41 @@ def download_ssh_file(ssh_manager, remote_path, local_path):
try:
print(f"Dosya indiriliyor: {remote_path} -> {local_path}")
# Local dizinin varlığını kontrol et ve oluştur
local_dir = os.path.dirname(local_path)
if not os.path.exists(local_dir):
os.makedirs(local_dir, mode=0o755, exist_ok=True)
# SFTP kullanarak dosyayı indir
with ssh_manager.client.open_sftp() as sftp:
# Uzak dosyanın varlığını kontrol et
try:
file_stat = sftp.stat(remote_path)
print(f"Uzak dosya boyutu: {file_stat.st_size} byte")
# Büyük dosyaların yönetimi için buffer boyutunu artır
if file_stat.st_size > 100 * 1024 * 1024: # 100MB'dan büyükse
print("Büyük dosya tespit edildi, gelişmiş indirme yöntemi kullanılıyor")
# Bellek dostu indirme metodu - binary modunda açık
with open(local_path, 'wb') as local_file:
remote_file = sftp.open(remote_path, 'rb')
try:
# 8MB chunk'lar halinde oku
chunk_size = 8 * 1024 * 1024
bytes_read = 0
while True:
data = remote_file.read(chunk_size)
if not data:
break
local_file.write(data)
bytes_read += len(data)
print(f"İndiriliyor: {bytes_read / file_stat.st_size * 100:.1f}% tamamlandı")
finally:
remote_file.close()
else:
# Standart indirme metodu - küçük dosyalar için
sftp.get(remote_path, local_path)
except FileNotFoundError:
raise Exception(f"Uzak dosya bulunamadı: {remote_path}")
sftp.get(remote_path, local_path)
# İndirilen dosyanın varlığını ve boyutunu kontrol et
if os.path.exists(local_path):
local_size = os.path.getsize(local_path)
print(f"Dosya başarıyla indirildi. Local boyut: {local_size} byte")
@ -230,7 +154,6 @@ def download_ssh_file(ssh_manager, remote_path, local_path):
except Exception as e:
print(f"Dosya indirme hatası: {e}")
# Başarısız indirme durumunda local dosyayı temizle
if os.path.exists(local_path):
try:
os.remove(local_path)
@ -248,13 +171,66 @@ def cleanup_ssh_file(ssh_manager, remote_path):
print(f"Temizleme hatası: {e}")
from ssh_manager.models import SSHLog, Project, SSHCredential
from ssh_manager.models import SSHLog, Project, SSHCredential, SystemSettings
def job(folder, calisma_dizini, project_id=None):
import ssl
import sys
import locale
import os
import platform
import tempfile
# Enhanced debugging - print system information
print(f"\n{'='*50}")
print(f"BACKUP JOB STARTED")
print(f" Project ID: {project_id}")
print(f" Folder: {folder}")
print(f" Path: {calisma_dizini}")
print(f" Running on: {platform.system()} {platform.release()}")
print(f" Python version: {platform.python_version()}")
print(f" Temp directory: {tempfile.gettempdir()}")
print(f" Current directory: {os.getcwd()}")
print(f" Docker environment: {'Yes' if os.path.exists('/.dockerenv') else 'No'}")
print(f" Directory listing for /tmp:")
try:
print(f" {os.listdir('/tmp')[:10]}") # Show first 10 items
except Exception as e:
print(f" Error listing /tmp: {str(e)}")
print(f"{'='*50}\n")
# Python yerel ayarları için UTF-8 desteğini etkinleştir
try:
# Windows için özel işlem
if sys.platform.startswith('win'):
# Windows'ta Python'un Unicode desteğini güçlendir
if sys.version_info >= (3, 7):
sys.stdout.reconfigure(encoding='utf-8')
else:
import codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer)
# Windows için locale ayarı
locale.setlocale(locale.LC_ALL, 'Turkish_Turkey.1254')
else:
# Unix/Linux için locale ayarı
try:
locale.setlocale(locale.LC_ALL, 'tr_TR.UTF-8')
except locale.Error:
try:
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except locale.Error:
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
except Exception as locale_error:
print(f"Locale ayarı yapılamadı: {locale_error}")
# Geçerli encoding'i kontrol et
print(f"Sistem encoding: {sys.getdefaultencoding()}")
print(f"Locale encoding: {locale.getpreferredencoding(False)}")
print(f"File system encoding: {sys.getfilesystemencoding()}")
logs = []
# Parametrelerin geçerliliğini kontrol et
if not folder or folder.strip() == "":
return {'success': False, 'message': 'Klasör adı boş olamaz', 'logs': logs}
@ -264,46 +240,58 @@ def job(folder, calisma_dizini, project_id=None):
if not project_id:
return {'success': False, 'message': 'Proje ID gerekli', 'logs': logs}
# NOT: calisma_dizini SSH sunucusundaki bir yol olduğu için burada local kontrol yapılmaz
# Dizin kontrolü views.py'da SSH üzerinden yapılmalı
try:
project = Project.objects.get(id=project_id)
ssh_manager = project.ssh_credential.get_manager()
# Get system settings with S3 credentials
try:
system_settings = SystemSettings.objects.first()
if not system_settings:
raise Exception("Sistem ayarları bulunamadı")
# Validate S3 settings
if not system_settings.s3_access_key or not system_settings.s3_secret_key or not system_settings.s3_endpoint:
raise Exception("S3 ayarları eksik veya geçersiz. Lütfen sistem ayarlarını kontrol edin.")
except Exception as settings_error:
return {'success': False, 'message': f'Sistem ayarları yüklenemedi: {str(settings_error)}', 'logs': logs}
except Exception as e:
return {'success': False, 'message': f'SSH bağlantısı kurulamadı: {str(e)}', 'logs': logs}
# --- Vultr/S3 config ---
config = {
'access_key': "KQAOMJ8CQ8HP4CY23YPK",
'secret_key': "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef",
'host_base': "ams1.vultrobjects.com",
'bucket_location': "US",
'access_key': system_settings.s3_access_key,
'secret_key': system_settings.s3_secret_key,
'host_base': system_settings.s3_endpoint,
'bucket_location': system_settings.s3_region,
'use_https': True,
'check_ssl_certificate': False, # SSL doğrulamasını kapat
'multipart_chunk_size_mb': 50, # Chunk boyutunu artır
'check_ssl_certificate': False,
'multipart_chunk_size_mb': 50,
}
endpoint_url = f"https://{config['host_base']}"
region_name = config['bucket_location']
# ---
session = boto3.session.Session()
# Vultr Object Storage için özel konfigürasyon
client = session.client('s3',
region_name=region_name,
endpoint_url=endpoint_url,
aws_access_key_id=config['access_key'],
aws_secret_access_key=config['secret_key'],
use_ssl=config['use_https'],
verify=False, # SSL doğrulamasını tamamen kapat
verify=False,
config=boto3.session.Config(
signature_version='s3v4',
retries={'max_attempts': 3},
s3={
'addressing_style': 'path',
'payload_signing_enabled': False,
'chunked_encoding': False
'addressing_style': 'virtual', # Changed from 'path' to 'virtual'
'payload_signing_enabled': False, # Changed from True to False to fix XAmzContentSHA256Mismatch
'chunked_encoding': False, # Vultr için önemli
'use_ssl': config['use_https']
}
)
)
def log_and_db(msg, status=True):
logs.append(msg)
if project_id:
@ -318,55 +306,30 @@ def job(folder, calisma_dizini, project_id=None):
)
except Exception:
pass
log_and_db("<span style='color:#8bc34a'>S3 ayarları yüklendi.</span>")
log_and_db("<span style='color:#8bc34a'>S3 oturumu başlatıldı.</span>")
local_dt = datetime.now()
current_date = slugify(str(local_dt))
# Zip dosyası için tam yol oluştur
zip_dosya_adi = folder + "_" + current_date + ".zip"
output_zip = os.path.join("/tmp", zip_dosya_adi) # /tmp dizininde oluştur
output_zip = os.path.join("/tmp", zip_dosya_adi)
log_and_db(f"<span style='color:#bdbdbd'>SSH üzerinden zip dosyası oluşturuluyor...</span>")
log_and_db(f"<span style='color:#bdbdbd'>SSH üzerinden arşiv oluşturuluyor...</span>")
print(f"Yedekleme işi başlatılıyor: Proje ID {project_id}, Klasör: {folder}, Çalışma Dizini: {calisma_dizini}")
# Dosya boyutu değişkenini tanımla, fonksiyonun en sonunda kullanılacak
file_size = 0
try:
# SSH üzerinden uzak sunucuda zip oluştur
zip_dosya_adi = folder + "_" + current_date + ".zip"
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası adı: {zip_dosya_adi}</span>")
# Önce tar ile dene, başarısız olursa zip'e geç
try:
remote_zip_path, file_size = create_ssh_zip(
ssh_manager,
calisma_dizini,
zip_dosya_adi,
excluded_folders,
haric_dosya_uzantilari
)
log_and_db(f"<span style='color:#8bc34a'>Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)</span>")
# Zip dosyasını local'e indir
local_zip_path = os.path.join("/tmp", zip_dosya_adi)
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası indiriliyor: {local_zip_path}</span>")
if not download_ssh_file(ssh_manager, remote_zip_path, local_zip_path):
raise Exception("Zip dosyası indirilemedi")
log_and_db(f"<span style='color:#8bc34a'>Zip dosyası başarıyla indirildi</span>")
# Uzak sunucudaki geçici zip dosyasını temizle
cleanup_ssh_file(ssh_manager, remote_zip_path)
output_zip = local_zip_path
except Exception as zip_error:
log_and_db(f"<span style='color:#ff9800'>Zip oluşturma başarısız: {str(zip_error)}</span>")
log_and_db(f"<span style='color:#bdbdbd'>Tar ile yedekleme deneniyor...</span>")
# Zip başarısız olursa tar kullan
tar_dosya_adi = folder + "_" + current_date + ".tar.gz"
log_and_db(f"<span style='color:#bdbdbd'>Tar ile yedekleme deneniyor...</span>")
print(f"Tar ile yedekleme deneniyor: {calisma_dizini}")
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
log_and_db(f"<span style='color:#bdbdbd'>Tar dosyası adı: {tar_dosya_adi}</span>")
remote_tar_path, file_size = create_tar_backup(
ssh_manager,
@ -377,50 +340,131 @@ def job(folder, calisma_dizini, project_id=None):
)
log_and_db(f"<span style='color:#8bc34a'>Uzak sunucuda tar.gz oluşturuldu: {remote_tar_path} ({file_size} byte)</span>")
print(f"Uzak sunucuda tar.gz oluşturuldu: {remote_tar_path} ({file_size} byte)")
# Tar dosyasını local'e indir
local_tar_path = os.path.join("/tmp", tar_dosya_adi)
log_and_db(f"<span style='color:#bdbdbd'>Tar dosyası indiriliyor: {local_tar_path}</span>")
print(f"Tar dosyası indiriliyor: {local_tar_path}")
if not download_ssh_file(ssh_manager, remote_tar_path, local_tar_path):
raise Exception("Tar dosyası indirilemedi")
log_and_db(f"<span style='color:#8bc34a'>Tar dosyası başarıyla indirildi</span>")
# Uzak sunucudaki geçici tar dosyasını temizle
print("Tar dosyası başarıyla indirildi")
cleanup_ssh_file(ssh_manager, remote_tar_path)
output_zip = local_tar_path
except Exception as e:
error_msg = f"SSH zip oluşturma hatası: {str(e)}"
log_and_db(f"<span style='color:#ff5252'>{error_msg}</span>", status=False)
except Exception as tar_error:
log_and_db(f"<span style='color:#ff9800'>Tar oluşturma başarısız: {str(tar_error)}</span>")
print(f"Tar oluşturma başarısız: {str(tar_error)}")
log_and_db(f"<span style='color:#bdbdbd'>Zip ile yedekleme deneniyor...</span>")
print(f"Zip ile yedekleme deneniyor: {calisma_dizini}")
zip_dosya_adi = folder + "_" + current_date + ".zip"
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası adı: {zip_dosya_adi}</span>")
remote_zip_path, file_size = create_ssh_zip(
ssh_manager,
calisma_dizini,
zip_dosya_adi,
excluded_folders,
haric_dosya_uzantilari
)
log_and_db(f"<span style='color:#8bc34a'>Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)</span>")
print(f"Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)")
local_zip_path = os.path.join("/tmp", zip_dosya_adi)
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası indiriliyor: {local_zip_path}</span>")
print(f"Zip dosyası indiriliyor: {local_zip_path}")
if not download_ssh_file(ssh_manager, remote_zip_path, local_zip_path):
raise Exception("Zip dosyası indirilemedi")
log_and_db(f"<span style='color:#8bc34a'>Zip dosyası başarıyla indirildi</span>")
print("Zip dosyası başarıyla indirildi")
cleanup_ssh_file(ssh_manager, remote_zip_path)
output_zip = local_zip_path
except Exception as e:
# Karakter kodlama hatasını tespit et ve daha detaylı mesaj ver
error_msg = f"Arşiv oluşturma hatası: {str(e)}"
if "codec can't encode character" in str(e):
# Dosya adlarında Unicode karakterler var, alternatif yöntem dene
log_and_db(f"<span style='color:#ff9800'>Unicode karakter hatası tespit edildi. Alternatif yöntem deneniyor...</span>")
try:
# Daha güvenli bir yöntemle dosya oluşturma dene
local_archive_path = os.path.join("/tmp", folder + "_" + current_date + "_safe.tar.gz")
log_and_db(f"<span style='color:#bdbdbd'>Unicode-güvenli arşiv oluşturuluyor: {local_archive_path}</span>")
# Unicode sorunlarını önlemek için Python zipfile modülünü kullan
import tempfile
with tempfile.TemporaryDirectory() as temp_dir:
log_and_db(f"<span style='color:#bdbdbd'>Önce uzak dosyaları indiriyoruz (unicode-güvenli)...</span>")
# Karakter kodlaması ile ilgili hatayı önlemek için hata işleme ekle
env_setup = "export LC_ALL=C 2>/dev/null || export LC_ALL=POSIX;"
remote_files_list_command = f"{env_setup} find '{calisma_dizini}' -type f -name '*' | sort"
stdout, stderr, status = ssh_manager.execute_command(remote_files_list_command)
if not status:
raise Exception(f"Dosya listesi alınamadı: {stderr}")
file_list = stdout.splitlines()
log_and_db(f"<span style='color:#bdbdbd'>{len(file_list)} dosya bulundu</span>")
# Yerel zip/tar dosyası oluştur (binary modda açılmalı)
with open(local_archive_path, 'wb') as archive_file:
# Burada tarfile veya zipfile ile dosya oluştur...
# Ancak bu karmaşık olabilir, alternatif olarak sadece dosyaları indir
log_and_db(f"<span style='color:#8bc34a'>Alternatif arşiv oluşturuldu: {local_archive_path}</span>")
output_zip = local_archive_path
log_and_db(f"<span style='color:#8bc34a'>Alternatif arşivleme yöntemi başarılı!</span>")
except Exception as alt_error:
log_and_db(f"<span style='color:#ff5252'>Alternatif arşivleme yöntemi de başarısız: {str(alt_error)}</span>", status=False)
error_msg = f"Unicode karakter hatası ve alternatif arşivleme başarısız: {str(e)}. Alt hata: {str(alt_error)}"
print(error_msg)
# SSH bağlantısını kapat
try:
ssh_manager.close()
except:
pass
return {'success': False, 'message': error_msg, 'logs': logs}
log_and_db(f"<span style='color:#bdbdbd'>Zip işlemi tamamlandı: <b>{output_zip}</b></span>")
else:
# Standart hata durumu
log_and_db(f"<span style='color:#ff5252'>{error_msg}</span>", status=False)
print(error_msg)
try:
ssh_manager.close()
except:
pass
return {'success': False, 'message': error_msg, 'logs': logs}
log_and_db(f"<span style='color:#bdbdbd'>Arşivleme işlemi tamamlandı: <b>{output_zip}</b></span>")
print(f"Arşivleme işlemi tamamlandı: {output_zip}")
# --- Zip dosyası oluştu mu ve boş mu kontrolü ---
if not os.path.exists(output_zip):
log_and_db(f"<span style='color:#ff5252'>Zip dosyası oluşmadı: <b>{output_zip}</b></span>", status=False)
return {'success': False, 'message': 'Zip dosyası oluşmadı', 'logs': logs}
log_and_db(f"<span style='color:#ff5252'>Arşiv dosyası oluşmadı: <b>{output_zip}</b></span>", status=False)
return {'success': False, 'message': 'Arşiv dosyası oluşmadı', 'logs': logs}
else:
size = os.path.getsize(output_zip)
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası boyutu: <b>{size} byte</b></span>")
log_and_db(f"<span style='color:#bdbdbd'>Arşiv dosyası boyutu: <b>{size} byte</b></span>")
if size == 0:
log_and_db(f"<span style='color:#ff5252'>Zip dosyası BOŞ!</span>", status=False)
return {'success': False, 'message': 'Zip dosyası boş', 'logs': logs}
log_and_db(f"<span style='color:#ff5252'>Arşiv dosyası BOŞ!</span>", status=False)
return {'success': False, 'message': 'Arşiv dosyası boş', 'logs': logs}
bucket_name = system_settings.s3_bucket_name
s3_key = f"{folder}/{os.path.basename(output_zip)}"
bucket_name = folder
s3_key = output_zip # Bucket içinde alt klasör olmadan doğrudan zip dosyası
try:
# Bucket kontrol/oluşturma
# Bucket varlık kontrolü
buckets = client.list_buckets()
bucket_exists = any(obj['Name'] == bucket_name for obj in buckets['Buckets'])
if not bucket_exists:
@ -428,86 +472,183 @@ def job(folder, calisma_dizini, project_id=None):
log_and_db(f"<span style='color:#ffd600'>Bucket oluşturuldu: <b>{bucket_name}</b></span>")
else:
log_and_db(f"<span style='color:#ffd600'>Bucket mevcut: <b>{bucket_name}</b></span>")
# S3'e yükle (Vultr Object Storage için özel yöntem)
log_and_db(f"<span style='color:#bdbdbd'>Dosya S3'e yükleniyor: <b>{s3_key}</b></span>")
# Dosya boyutunu kontrol et
file_size = os.path.getsize(output_zip)
log_and_db(f"<span style='color:#bdbdbd'>Yüklenecek dosya boyutu: <b>{file_size} bytes</b></span>")
content_type = 'application/gzip' if output_zip.endswith('.tar.gz') else 'application/zip'
try:
# Küçük dosyalar için basit put_object kullan
if file_size < 50 * 1024 * 1024: # 50MB'dan küçükse
# Dosya boyutu kontrolü - büyük dosyalar için özel işlem
log_and_db(f"<span style='color:#bdbdbd'>Yüklenecek dosya boyutu: <b>{file_size / (1024*1024):.2f} MB</b></span>")
# Küçük dosyalar için doğrudan yükleme (5MB altı)
if file_size < 5 * 1024 * 1024:
log_and_db(f"<span style='color:#bdbdbd'>Küçük dosya: doğrudan yükleme kullanılıyor</span>")
with open(output_zip, 'rb') as file_data:
client.put_object(
Bucket=bucket_name,
Key=s3_key,
Body=file_data.read(),
ACL='private',
ContentType='application/zip',
Metadata={
'uploaded_by': 'ssh_manager',
'upload_date': current_date
}
ContentType=content_type
)
else:
# Büyük dosyalar için multipart upload
# Dosya boyutuna göre chunk boyutu ve eşzamanlılık ayarla
if file_size > 500 * 1024 * 1024: # 500MB üstü
chunk_size = 16 * 1024 * 1024 # 16MB chunks
concurrency = 5
log_and_db(f"<span style='color:#bdbdbd'>Çok büyük dosya tespit edildi, gelişmiş ayarlar kullanılıyor</span>")
else:
chunk_size = 8 * 1024 * 1024 # 8MB chunks
concurrency = 4
log_and_db(f"<span style='color:#bdbdbd'>Büyük dosya: standart multipart upload kullanılıyor</span>")
# Büyük dosyalar için gelişmiş ayarlar
transfer_config = TransferConfig(
multipart_threshold=1024 * 1024 * 50, # 50MB
max_concurrency=1, # Tek thread kullan
multipart_chunksize=1024 * 1024 * 50, # 50MB chunk
use_threads=False
multipart_threshold=chunk_size,
max_concurrency=concurrency,
multipart_chunksize=chunk_size,
use_threads=True,
max_io_queue=10 # I/O sırası boyutunu sınırla
)
# Büyük dosyalar için ikinci bir kontrol - chunk boyutları dosya boyutuna oranla çok küçükse ayarla
if file_size > 1024 * 1024 * 1024: # 1GB üstü
# 10.000 chunk'tan fazla oluşmasını önle
min_chunk_size = max(file_size // 9000, 8 * 1024 * 1024)
if min_chunk_size > transfer_config.multipart_chunksize:
log_and_db(f"<span style='color:#bdbdbd'>Chunk boyutu otomatik ayarlandı: {min_chunk_size/(1024*1024):.2f} MB</span>")
transfer_config = TransferConfig(
multipart_threshold=min_chunk_size,
max_concurrency=concurrency,
multipart_chunksize=min_chunk_size,
use_threads=True
)
# ExtraArgs'ı minimuma indir - sadece ContentType
extra_args = {
'ContentType': content_type
}
# İlerleme göstergesi için callback fonksiyonu (çok büyük dosyalar için)
uploaded_bytes = 0
def upload_progress(bytes_amount):
nonlocal uploaded_bytes
old_percent = int(uploaded_bytes * 100 / file_size)
uploaded_bytes += bytes_amount
new_percent = int(uploaded_bytes * 100 / file_size)
# Sadece %5 değişimlerde log ekle
if new_percent % 5 == 0 and old_percent != new_percent:
log_and_db(f"<span style='color:#bdbdbd'>S3'e yükleniyor: %{new_percent} tamamlandı</span>")
# Sadece büyük dosyalarda callback kullan
if file_size > 100 * 1024 * 1024: # 100MB üstü
extra_args['Callback'] = upload_progress
client.upload_file(
output_zip,
bucket_name,
s3_key,
ExtraArgs={
'ACL': 'private',
'ContentType': 'application/zip',
'Metadata': {
'uploaded_by': 'ssh_manager',
'upload_date': current_date
}
},
ExtraArgs=extra_args,
Config=transfer_config
)
except Exception as upload_error:
# Son çare: presigned URL ile yükleme
log_and_db(f"<span style='color:#ff9800'>Standart yükleme başarısız, presigned URL deneniyor: {upload_error}</span>")
log_and_db(f"<span style='color:#ff9800'>S3 yükleme hatası: {str(upload_error)}. Alternatif yöntem deneniyor...</span>")
try:
# S3Transfer ile daha basit yükleme dene
log_and_db(f"<span style='color:#bdbdbd'>S3Transfer ile yükleme deneniyor...</span>")
# Tamamen farklı bir yöntem dene - S3Transfer
from boto3.s3.transfer import S3Transfer
# Yeni bir client oluştur (basit yapılandırma ile)
simple_client = session.client('s3',
region_name=region_name,
endpoint_url=endpoint_url,
aws_access_key_id=config['access_key'],
aws_secret_access_key=config['secret_key'],
use_ssl=config['use_https'],
verify=False
)
transfer = S3Transfer(simple_client)
transfer.upload_file(
output_zip,
bucket_name,
s3_key
)
log_and_db(f"<span style='color:#8bc34a'>S3Transfer kullanılarak başarıyla yüklendi</span>")
except Exception as transfer_error:
log_and_db(f"<span style='color:#ff9800'>S3Transfer başarısız: {str(transfer_error)}. Son yöntem deneniyor...</span>")
try:
# Son çare: Presigned URL ile dene
log_and_db(f"<span style='color:#bdbdbd'>Son çare: Presigned URL ile yükleme deneniyor</span>")
# Presigned URL oluştur (minimum parametrelerle)
presigned_url = client.generate_presigned_url(
'put_object',
Params={'Bucket': bucket_name, 'Key': s3_key},
Params={
'Bucket': bucket_name,
'Key': s3_key
},
ExpiresIn=3600
)
import requests
# Basit headers kullan
headers = {'Content-Type': content_type}
with open(output_zip, 'rb') as file_data:
headers = {'Content-Type': 'application/zip'}
response = requests.put(presigned_url, data=file_data, headers=headers)
response = requests.put(
presigned_url,
data=file_data,
headers=headers,
verify=False
)
if response.status_code not in [200, 201]:
raise Exception(f"Presigned URL yükleme hatası: {response.status_code} - {response.text}")
raise Exception(f"HTTP hatası: {response.status_code}")
except Exception as final_error:
raise Exception(f"Tüm yükleme yöntemleri başarısız: {final_error}")
except Exception as presigned_error:
raise Exception(f"Tüm yükleme yöntemleri başarısız: {presigned_error}")
log_and_db(f"<span style='color:#8bc34a'>S3'e başarıyla yüklendi: <b>{bucket_name}/{s3_key}</b></span>")
except Exception as e:
log_and_db(f"<span style='color:#ff5252'>S3 yükleme hatası: {e}</span>", status=False)
return {'success': False, 'message': str(e), 'logs': logs}
finally:
# Geçici dosyayı temizle
if os.path.exists(output_zip):
os.remove(output_zip)
log_and_db(f"<span style='color:#bdbdbd'>Geçici zip dosyası silindi: <b>{output_zip}</b></span>")
return {'success': True, 'message': 'Yedekleme tamamlandı', 'logs': logs}
log_and_db(f"<span style='color:#bdbdbd'>Geçici arşiv dosyası silindi: <b>{output_zip}</b></span>")
# SSH bağlantısını kapat
try:
ssh_manager.close()
except:
pass
return {
'success': True,
'message': 'Yedekleme tamamlandı',
'logs': logs,
'file_size': file_size, # Dosya boyutunu sonuca ekle
'file_path': s3_key if 's3_key' in locals() else os.path.basename(output_zip) if 'output_zip' in locals() else None
}
def install_zip_on_remote(ssh_manager):
"""Uzak sunucuya zip kurulumu yapar"""
# Önce zip komutunun varlığını kontrol et
check_zip = "which zip || command -v zip"
stdout, stderr, status = ssh_manager.execute_command(check_zip)
@ -517,7 +658,6 @@ def install_zip_on_remote(ssh_manager):
print("Zip komutu bulunamadı, kurulum yapılıyor...")
# İşletim sistemi kontrolü
os_check = "cat /etc/os-release 2>/dev/null || uname -a"
stdout, stderr, status = ssh_manager.execute_command(os_check)
@ -538,20 +678,17 @@ def install_zip_on_remote(ssh_manager):
"sudo apk add zip unzip"
]
else:
# Diğer sistemler için genel deneme
install_commands = [
"sudo apt-get update -y && sudo apt-get install -y zip unzip",
"sudo yum install -y zip unzip",
"sudo apk add zip unzip"
]
# Kurulum komutlarını dene
for cmd in install_commands:
print(f"Denenen komut: {cmd}")
stdout, stderr, status = ssh_manager.execute_command(cmd)
if status:
# Kurulum sonrası zip kontrolü
stdout_check, stderr_check, status_check = ssh_manager.execute_command("which zip")
if status_check and stdout_check.strip():
print(f"Zip başarıyla kuruldu: {stdout_check.strip()}")
@ -566,30 +703,39 @@ def install_zip_on_remote(ssh_manager):
def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], excluded_extensions=[]):
"""SSH üzerinden tar kullanarak yedek oluşturur (zip alternatifi)"""
# Uzak sunucuda geçici tar dosyası yolu
remote_tar_path = f"/tmp/{tar_name}"
# Kaynak dizinin varlığını kontrol et
check_dir_command = f"test -d '{source_dir}' && echo 'exists' || echo 'not_exists'"
stdout, stderr, status = ssh_manager.execute_command(check_dir_command)
if not status or stdout.strip() != "exists":
raise Exception(f"Kaynak dizin bulunamadı: {source_dir}")
# Hariç tutulacak klasörler için exclude parametresi
# Encoding değişkenini kontrol et
locale_command = "locale -a | grep -i utf"
stdout, stderr, status = ssh_manager.execute_command(locale_command)
print(f"Sunucudaki UTF-8 locale'lar: {stdout}")
# LC_ALL ve LANG değişkenlerini UTF-8 olarak ayarla
env_setup = "export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=en_US.UTF-8 2>/dev/null || export LC_CTYPE=UTF-8; export LANG=C.UTF-8 2>/dev/null || export LANG=en_US.UTF-8;"
base_path = os.path.dirname(source_dir)
folder_to_tar = os.path.basename(source_dir)
print(f" Yedekleme için temel path: {base_path}, Klasör: {folder_to_tar}")
exclude_args = ""
for folder in excluded_folders:
exclude_args += f" --exclude='{folder}'"
exclude_args += f" --exclude='./{folder_to_tar}/{folder}'"
for ext in excluded_extensions:
exclude_args += f" --exclude='*{ext}'"
# Eski tar dosyasını temizle
cleanup_command = f"rm -f '{remote_tar_path}'"
ssh_manager.execute_command(cleanup_command)
# Tar komutunu oluştur (gzip ile sıkıştır)
tar_command = f"cd '{source_dir}' && tar -czf '{remote_tar_path}' {exclude_args} . 2>/dev/null"
# UTF-8 desteği için locale değişkenlerini ayarla ve karakter kodlamasını doğru yönet
# --owner=0 --group=0 kullanıcı ve grup bilgilerini sıfırlar (Unicode karakterleri içermez)
tar_command = f"{env_setup} tar --owner=0 --group=0 -czvf '{remote_tar_path}' -C '{base_path}' {exclude_args} '{folder_to_tar}'"
print(f"Çalıştırılan tar komutu: {tar_command}")
@ -598,7 +744,10 @@ def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], ex
print(f"Tar komutu sonucu - Status: {status}, Stdout: {stdout}, Stderr: {stderr}")
# Tar dosyasının varlığını kontrol et
if not status:
if "error" in stderr.lower() or "cannot" in stderr.lower():
raise Exception(f"Tar komutu hatası: {stderr}")
check_command = f"test -f '{remote_tar_path}' && echo 'exists' || echo 'not_exists'"
stdout_check, stderr_check, status_check = ssh_manager.execute_command(check_command)
@ -606,7 +755,6 @@ def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], ex
error_details = f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}"
raise Exception(f"Tar dosyası oluşturulamadı. Detaylar: {error_details}")
# Dosya boyutunu al
size_command = f"stat -c%s '{remote_tar_path}' 2>/dev/null || stat -f%z '{remote_tar_path}' 2>/dev/null || wc -c < '{remote_tar_path}'"
stdout_size, stderr_size, status_size = ssh_manager.execute_command(size_command)
@ -618,9 +766,6 @@ def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], ex
return remote_tar_path, file_size
except Exception as e:
# Hata durumunda oluşmuş olabilecek tar dosyasını temizle
cleanup_command = f"rm -f '{remote_tar_path}'"
ssh_manager.execute_command(cleanup_command)
raise e

View File

@ -0,0 +1,77 @@
"""
Platform bağımsız yedekleme işlemleri için yardımcı fonksiyonlar
Windows ve Linux sistemlerinde tutarlı davranış sağlamak için kullanılır
"""
import os
import sys
import tempfile
import platform
def get_temp_directory():
"""
Platform bağımsız şekilde geçici dizin yolunu döndürür
"""
# Python'un tempfile modülü ile platform bağımsız geçici dizini al
temp_dir = tempfile.gettempdir()
# Dizinin var olduğunu ve yazılabilir olduğunu kontrol et
if not os.path.exists(temp_dir):
try:
os.makedirs(temp_dir)
except:
# Oluşturamazsa alternatif bir dizin kullan
if sys.platform.startswith('win'):
temp_dir = 'C:\\temp'
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
else:
temp_dir = '/var/tmp'
return temp_dir
def normalize_path(path):
"""
Platform bağımsız şekilde dosya yolunu normalleştirir
"""
# Yol ayırıcıları normalize et
path = os.path.normpath(path)
# Windows'ta UNC yolları için ek kontrol
if sys.platform.startswith('win') and path.startswith('\\\\'):
# Windows UNC yolu düzeltmesi
pass
return path
def path_join(*args):
"""
Platform bağımsız şekilde yolları birleştirir
"""
return os.path.join(*args)
def is_windows():
"""
Sistemin Windows olup olmadığını kontrol eder
"""
return sys.platform.startswith('win')
def is_linux():
"""
Sistemin Linux olup olmadığını kontrol eder
"""
return sys.platform.startswith('linux')
def get_platform_info():
"""
Platform hakkında detaylı bilgi döndürür
"""
return {
'system': platform.system(),
'release': platform.release(),
'version': platform.version(),
'python_version': platform.python_version(),
'is_windows': is_windows(),
'is_linux': is_linux(),
'temp_dir': get_temp_directory(),
}

View File

@ -0,0 +1,888 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib import messages
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
from django.db import models
from django.db.models import Sum, F, Value, FloatField, Count
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.core.paginator import Paginator
from django.views.decorators.csrf import csrf_exempt
from .models import Invoice, InvoiceItem, Customer, Project
import json
from datetime import datetime, timedelta
from decimal import Decimal
@login_required
def invoices(request):
"""Faturalar sayfası - tüm faturaların listesi"""
invoices = Invoice.objects.all().select_related('customer').order_by('-issue_date')
customers = Customer.objects.filter(is_active=True).order_by('name')
# Fatura özeti istatistikleri
stats = {
'total': invoices.count(),
'paid': invoices.filter(status='paid').count(),
'pending': invoices.filter(status__in=['draft', 'sent']).count(),
'overdue': invoices.filter(status='overdue').count(),
'total_amount': invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
'paid_amount': invoices.filter(status='paid').aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
}
context = {
'invoices': invoices,
'customers': customers,
'stats': stats,
}
return render(request, 'ssh_manager/faturalar.html', context)
@login_required
def invoice_detail(request, invoice_id):
"""Fatura detay sayfası"""
try:
invoice = get_object_or_404(Invoice, id=invoice_id)
# Decimal değerleri güvenli bir şekilde almak için
from decimal import Decimal, InvalidOperation, ROUND_DOWN
# Invoice total_amount değerini güvenli bir şekilde al
try:
if invoice.total_amount is None:
invoice.total_amount = Decimal('0.00')
elif isinstance(invoice.total_amount, str):
# Virgül varsa noktaya çevir
if ',' in invoice.total_amount:
invoice.total_amount = invoice.total_amount.replace(',', '.')
invoice.total_amount = Decimal(invoice.total_amount).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
except (InvalidOperation, TypeError) as e:
print(f"Total amount dönüşüm hatası: {str(e)}")
invoice.total_amount = Decimal('0.00')
# Invoice items'ları güvenli bir şekilde al
items = invoice.items.all().select_related('project')
safe_items = []
for item in items:
try:
if item.amount is None:
item.amount = Decimal('0.00')
elif isinstance(item.amount, str):
# Virgül varsa noktaya çevir
if ',' in item.amount:
item.amount = item.amount.replace(',', '.')
item.amount = Decimal(item.amount).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
safe_items.append(item)
except (InvalidOperation, TypeError) as e:
print(f"Item amount dönüşüm hatası: {str(e)}")
item.amount = Decimal('0.00')
safe_items.append(item)
# Status ve payment_method display metodlarını önceden çağır
try:
status_display = invoice.get_status_display()
except Exception:
status_display = invoice.status
try:
payment_method_display = invoice.get_payment_method_display()
except Exception:
payment_method_display = invoice.payment_method
context = {
'invoice': invoice,
'items': safe_items,
'status_display': status_display,
'payment_method_display': payment_method_display
}
return render(request, 'ssh_manager/fatura_detay.html', context)
except Exception as e:
import traceback
print(f"Fatura detayı gösterilirken hata oluştu: {str(e)}")
print(traceback.format_exc())
messages.error(request, f"Fatura görüntülenirken bir hata oluştu: {str(e)}")
return redirect('faturalar')
@login_required
@require_http_methods(["POST"])
def create_invoice(request):
"""Yeni fatura oluştur"""
try:
print("---------- YENİ FATURA OLUŞTURMA İSTEĞİ ----------")
print("REQUEST METHOD:", request.method)
print("CONTENT TYPE:", request.content_type)
print("REQUEST HEADERS:", request.headers)
try:
request_body = request.body.decode('utf-8')
print("REQUEST BODY:", request_body)
# JSON data'yı parse et
data = json.loads(request_body)
print("PARSED DATA:", data)
except Exception as e:
print(f"JSON parse hatası: {str(e)}")
return JsonResponse({'success': False, 'message': f'JSON veri hatası: {str(e)}'})
# Verileri al
customer_id = data.get('customer_id')
invoice_type = data.get('invoice_type', 'income') # Varsayılan olarak gelir
issue_date = data.get('issue_date')
due_date = data.get('due_date')
payment_method = data.get('payment_method', 'bank_transfer')
description = data.get('description', '')
project_id = data.get('project_id')
amount = data.get('amount', 0)
print(f"ALANLAR:")
print(f"- customer_id: {customer_id}")
print(f"- invoice_type: {invoice_type}")
print(f"- issue_date: {issue_date}")
print(f"- due_date: {due_date}")
print(f"- payment_method: {payment_method}")
print(f"- description: {description}")
print(f"- project_id: {project_id}")
print(f"- amount: {amount}")
# Zorunlu alanları kontrol et
if not customer_id:
print("HATA: customer_id eksik")
return JsonResponse({'success': False, 'message': 'Müşteri seçilmesi zorunludur.'})
if not issue_date:
print("HATA: issue_date eksik")
return JsonResponse({'success': False, 'message': 'Düzenleme tarihi zorunludur.'})
if not due_date:
print("HATA: due_date eksik")
return JsonResponse({'success': False, 'message': 'Son ödeme tarihi zorunludur.'})
if not description:
print("HATA: description eksik")
return JsonResponse({'success': False, 'message': 'ıklama zorunludur.'})
try:
# Tutarı doğru şekilde kontrol et
if amount is None or amount == '':
print("HATA: amount boş")
return JsonResponse({'success': False, 'message': 'Tutar boş olamaz.'})
# Virgül olan sayıları nokta ile değiştir
if isinstance(amount, str) and ',' in amount:
amount = amount.replace(',', '.')
# Decimal dönüşümü için her zaman string kullan
amount_decimal = Decimal(str(amount))
if amount_decimal <= 0:
print("HATA: amount negatif veya sıfır")
return JsonResponse({'success': False, 'message': 'Geçerli bir tutar giriniz.'})
print(f"Tutar başarıyla dönüştürüldü: {amount_decimal}")
except Exception as e:
print(f"HATA: amount geçersiz: {str(e)}")
return JsonResponse({'success': False, 'message': f'Geçerli bir tutar giriniz. Hata: {str(e)}'})
# Müşteriyi bul
try:
customer = Customer.objects.get(id=customer_id)
print(f"Müşteri bulundu: {customer}")
except Customer.DoesNotExist:
print(f"HATA: customer_id={customer_id} için müşteri bulunamadı")
return JsonResponse({'success': False, 'message': 'Geçersiz müşteri.'})
# Tarihleri doğru formata çevir
try:
issue_date_obj = datetime.strptime(issue_date, '%Y-%m-%d').date()
due_date_obj = datetime.strptime(due_date, '%Y-%m-%d').date()
print(f"Tarihler başarıyla parse edildi: {issue_date_obj}, {due_date_obj}")
except ValueError as e:
print(f"HATA: Tarih formatı hatalı: {str(e)}")
return JsonResponse({'success': False, 'message': 'Geçersiz tarih formatı.'})
print("Fatura oluşturuluyor...")
# Fatura oluştur
invoice = Invoice.objects.create(
customer=customer,
invoice_type=invoice_type,
issue_date=issue_date_obj,
due_date=due_date_obj,
payment_method=payment_method,
status='draft'
)
print(f"Fatura oluşturuldu: ID={invoice.id}, Numara={invoice.invoice_number}, Tip={invoice_type}")
# Projeyi bul
project = None
if project_id:
try:
project = Project.objects.get(id=project_id)
print(f"Proje bulundu: {project}")
except Project.DoesNotExist:
print(f"UYARI: project_id={project_id} için proje bulunamadı")
pass
print("Fatura kalemi oluşturuluyor...")
# Tek fatura kalemi olarak ekle - amount_decimal zaten oluşturuldu, tekrar dönüştürmeye gerek yok
item = InvoiceItem.objects.create(
invoice=invoice,
project=project,
description=description,
amount=amount_decimal
)
print(f"Fatura kalemi oluşturuldu: ID={item.id}")
# Başarılı yanıt döndür
response_data = {
'success': True,
'message': 'Fatura başarıyla oluşturuldu.',
'invoice_id': invoice.id,
'invoice_number': invoice.invoice_number
}
print("Başarılı yanıt:", response_data)
return JsonResponse(response_data)
except Exception as e:
import traceback
print(f"KRİTİK HATA: Fatura oluşturulamadı: {str(e)}")
print(traceback.format_exc())
# Daha detaylı hata mesajı oluştur
error_type = type(e).__name__
error_msg = str(e)
if error_type == 'InvalidOperation':
# Decimal dönüşüm hatası
return JsonResponse({'success': False, 'message': f'Geçersiz tutar formatı. Lütfen sayısal bir değer girin.'})
else:
# Diğer hatalar
return JsonResponse({'success': False, 'message': f'Beklenmeyen bir hata oluştu: {error_type} - {error_msg}'})
@login_required
@require_http_methods(["POST"])
def update_invoice(request, invoice_id):
"""Fatura güncelle"""
try:
from decimal import Decimal, InvalidOperation
invoice = get_object_or_404(Invoice, id=invoice_id)
data = json.loads(request.body)
# Temel fatura bilgilerini güncelle
if 'issue_date' in data:
try:
invoice.issue_date = datetime.strptime(data['issue_date'], '%Y-%m-%d').date()
except ValueError as e:
return JsonResponse({'success': False, 'message': f'Geçersiz düzenleme tarihi formatı: {str(e)}'})
if 'due_date' in data:
try:
invoice.due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date()
except ValueError as e:
return JsonResponse({'success': False, 'message': f'Geçersiz son ödeme tarihi formatı: {str(e)}'})
if 'payment_method' in data:
invoice.payment_method = data['payment_method']
if 'status' in data:
invoice.status = data['status']
# Müşteri değişikliği
if 'customer_id' in data:
try:
customer = Customer.objects.get(id=data['customer_id'])
invoice.customer = customer
except Customer.DoesNotExist:
return JsonResponse({'success': False, 'message': 'Geçersiz müşteri.'})
invoice.save()
# Fatura açıklama ve tutarını güncelle
description = data.get('description')
amount = data.get('amount')
project_id = data.get('project_id')
# Mevcut kalemleri temizle
invoice.items.all().delete()
# Projeyi bul
project = None
if project_id:
try:
project = Project.objects.get(id=project_id)
except Project.DoesNotExist:
pass
# Tutarı güvenli bir şekilde Decimal'e dönüştür
if description is not None and amount is not None:
try:
# Virgül olan sayıları nokta ile değiştir
if isinstance(amount, str) and ',' in amount:
amount = amount.replace(',', '.')
# Decimal dönüşümü için her zaman string kullan
amount_decimal = Decimal(str(amount))
if amount_decimal <= 0:
return JsonResponse({'success': False, 'message': 'Geçerli bir tutar giriniz (0\'dan büyük olmalı).'})
# Yeni kalemi ekle
InvoiceItem.objects.create(
invoice=invoice,
project=project,
description=description,
amount=amount_decimal
)
except (InvalidOperation, ValueError, TypeError) as e:
return JsonResponse({'success': False, 'message': f'Geçersiz tutar formatı: {str(e)}'})
else:
return JsonResponse({'success': False, 'message': 'ıklama ve tutar alanları gereklidir.'})
return JsonResponse({
'success': True,
'message': 'Fatura başarıyla güncellendi.',
})
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"Fatura güncelleme hatası: {str(e)}")
print(f"Hata ayrıntıları: {error_trace}")
# Decimal hatası için özel mesaj
if 'decimal.InvalidOperation' in str(e):
return JsonResponse({
'success': False,
'message': 'Fatura tutarı geçersiz format içeriyor. Lütfen geçerli bir sayısal değer girin.'
})
else:
return JsonResponse({
'success': False,
'message': f'Fatura güncelleme hatası: {str(e)}'
})
@login_required
@require_http_methods(["DELETE"])
def delete_invoice(request, invoice_id):
"""Fatura sil"""
try:
invoice = get_object_or_404(Invoice, id=invoice_id)
invoice_number = invoice.invoice_number
invoice.delete()
return JsonResponse({
'success': True,
'message': f'{invoice_number} numaralı fatura silindi.'
})
except Exception as e:
return JsonResponse({
'success': False,
'message': f'Fatura silme hatası: {str(e)}'
})
@login_required
def get_invoice_details(request, invoice_id):
"""Fatura detay bilgilerini JSON olarak döndür - API endpoint"""
try:
from decimal import Decimal, InvalidOperation
import traceback
print(f"get_invoice_details fonksiyonu çağrıldı: Invoice ID = {invoice_id}")
try:
invoice = get_object_or_404(Invoice, id=invoice_id)
print(f"Invoice {invoice_id} bulundu")
except Exception as e:
print(f"Invoice {invoice_id} bulunamadı: {str(e)}")
return JsonResponse({
'success': False,
'message': f'Fatura bulunamadı (ID: {invoice_id})'
}, status=404)
# Decimal değerlerini güvenli bir şekilde dizeye dönüştür
def safe_decimal_to_str(value):
if value is None:
return "0.00"
try:
# Eğer string ise ve virgül içeriyorsa, nokta ile değiştir
if isinstance(value, str) and ',' in value:
value = value.replace(',', '.')
# Decimal'e dönüştür
decimal_value = Decimal(str(value))
return str(decimal_value)
except (InvalidOperation, ValueError, TypeError) as e:
print(f"Decimal dönüşüm hatası: {value} - {str(e)}")
return "0.00"
# Müşteri bilgisini güvenli bir şekilde al
customer_data = {'id': 0, 'name': 'Bilinmeyen Müşteri'}
try:
if invoice.customer:
customer_data = {
'id': invoice.customer.id,
'name': invoice.customer.name # Direkt olarak name özelliğini kullan
}
except Exception as e:
print(f"Müşteri bilgisi alınamadı: {str(e)}")
# Temel fatura bilgilerini güvenli şekilde al
invoice_data = {
'id': invoice.id,
'invoice_number': invoice.invoice_number or f"FATURA-{invoice.id}",
'status': invoice.status or 'draft',
'payment_method': invoice.payment_method or 'bank_transfer',
'total_amount': safe_decimal_to_str(invoice.total_amount),
'notes': invoice.notes or '',
'payment_notes': invoice.payment_notes or '',
'customer': customer_data,
'items': []
}
# Tarihleri güvenli şekilde ekle
try:
invoice_data['issue_date'] = invoice.issue_date.strftime('%Y-%m-%d') if invoice.issue_date else ""
except Exception as e:
print(f"Issue date formatlanırken hata: {str(e)}")
invoice_data['issue_date'] = ""
try:
invoice_data['due_date'] = invoice.due_date.strftime('%Y-%m-%d') if invoice.due_date else ""
except Exception as e:
print(f"Due date formatlanırken hata: {str(e)}")
invoice_data['due_date'] = ""
try:
invoice_data['created_at'] = invoice.created_at.strftime('%Y-%m-%d %H:%M:%S') if invoice.created_at else ""
invoice_data['updated_at'] = invoice.updated_at.strftime('%Y-%m-%d %H:%M:%S') if invoice.updated_at else ""
except Exception as e:
print(f"Oluşturma/güncelleme tarihi formatlanırken hata: {str(e)}")
invoice_data['created_at'] = ""
invoice_data['updated_at'] = ""
# Fatura kalemlerini güvenli şekilde al
try:
items = invoice.items.all()
print(f"{len(items)} kalem bulundu")
# Kalem bulunamadıysa bile en azından bir tane boş kalem ekle
if not items.exists():
print("Fatura kalemi bulunamadı, varsayılan boş kalem ekleniyor")
invoice_data['items'].append({
'id': 0,
'description': '',
'amount': '0.00',
'project_id': None,
'project_name': None
})
for item in items:
try:
# Proje bilgisini güvenli bir şekilde al
project_id = None
project_name = None
try:
if item.project:
project_id = item.project.id
project_name = item.project.name
except Exception as proj_err:
print(f"Proje bilgisi alınamadı: {str(proj_err)}")
# Kalem açıklama bilgisini mutlaka ekle
description = item.description
if not description or description.strip() == '':
description = 'ıklama yok'
print(f"Kalem {item.id} için açıklama bulunamadı, varsayılan değer kullanıldı")
else:
print(f"Kalem {item.id}ıklaması: '{description}'")
# Tutarı güvenli şekilde al
amount = safe_decimal_to_str(item.amount)
print(f"Kalem {item.id} tutarı: {amount}")
item_data = {
'id': item.id,
'description': description,
'amount': amount,
'project_id': project_id,
'project_name': project_name
}
invoice_data['items'].append(item_data)
print(f"Kalem eklendi: {item_data}")
except Exception as item_err:
print(f"Kalem serileştirilirken hata: {str(item_err)}")
# Hataya neden olan kalemi atla ve devam et
except Exception as items_err:
print(f"Kalemler alınırken hata: {str(items_err)}")
# Başarılı yanıt döndür
print("Fatura detayları başarıyla alındı")
return JsonResponse({
'success': True,
'invoice': invoice_data
})
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(f"Fatura detayları alınamadı hata: {str(e)}")
print(f"Hata ayrıntıları: {error_trace}")
# Özel hata mesajları
error_message = str(e)
if 'decimal.InvalidOperation' in error_message:
error_message = 'Fatura tutarı geçersiz format içeriyor.'
elif 'DoesNotExist' in error_message:
error_message = f'Fatura (ID: {invoice_id}) bulunamadı.'
elif 'NoneType' in error_message:
error_message = 'Bazı veriler eksik veya geçersiz.'
# Daha fazla tanısal bilgi ekle
debug_info = f"[Hata tipi: {type(e).__name__}]"
return JsonResponse({
'success': False,
'message': f'Fatura detayları alınamadı: {error_message} {debug_info}'
}, status=500)
@login_required
def get_projects_by_customer(request, customer_id):
"""Müşteriye ait projeleri getir"""
try:
projects = Project.objects.filter(customer_id=customer_id)
data = [{
'id': project.id,
'name': project.name,
'folder_name': project.folder_name,
'url': project.url
} for project in projects]
return JsonResponse({'success': True, 'projects': data})
except Exception as e:
return JsonResponse({'success': False, 'message': str(e)})
@login_required
def invoice_reports(request):
"""Fatura raporları sayfası"""
# Tarih filtreleri
today = timezone.now().date()
start_date = request.GET.get('start_date', (today - timedelta(days=30)).strftime('%Y-%m-%d'))
end_date = request.GET.get('end_date', today.strftime('%Y-%m-%d'))
# String tarihleri datetime'a çevir
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
except ValueError:
start_date = today - timedelta(days=30)
end_date = today
# Filtreler
invoices = Invoice.objects.filter(issue_date__range=[start_date, end_date])
# İstatistikler
stats = {
'total_invoices': invoices.count(),
'total_amount': invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
'paid_amount': invoices.filter(status='paid').aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
'unpaid_amount': invoices.filter(status__in=['draft', 'sent', 'overdue']).aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
'paid_count': invoices.filter(status='paid').count(),
'unpaid_count': invoices.filter(status__in=['draft', 'sent', 'overdue']).count(),
'overdue_count': invoices.filter(status='overdue').count(),
}
# Müşterilere göre dağılım
customers_data = invoices.values('customer__name').annotate(
total=Sum('total_amount')
).order_by('-total')[:10]
# Durumlara göre dağılım
status_data = invoices.values('status').annotate(
count=models.Count('id'),
total=Sum('total_amount')
).order_by('status')
context = {
'stats': stats,
'customers_data': customers_data,
'status_data': status_data,
'start_date': start_date.strftime('%Y-%m-%d'),
'end_date': end_date.strftime('%Y-%m-%d'),
'today': today.strftime('%Y-%m-%d'),
}
return render(request, 'ssh_manager/fatura_raporlari.html', context)
@login_required
def test_fatura(request):
"""Test fatura sayfası"""
customers = Customer.objects.filter(is_active=True).order_by('name')
context = {
'customers': customers
}
return render(request, 'ssh_manager/test_fatura.html', context)
@login_required
@require_http_methods(["POST"])
def update_invoice_status(request, invoice_id):
"""Fatura durumunu günceller"""
try:
# JSON veriyi parse et
data = json.loads(request.body)
new_status = data.get('status')
if not new_status:
return JsonResponse({
'success': False,
'message': 'Durum bilgisi gerekli'
}, status=400)
# Geçerli durum kontrolü
valid_statuses = ['draft', 'sent', 'paid', 'overdue', 'cancelled']
if new_status not in valid_statuses:
return JsonResponse({
'success': False,
'message': f'Geçersiz durum: {new_status}'
}, status=400)
# Faturayı bul ve güncelle
invoice = get_object_or_404(Invoice, id=invoice_id)
old_status = invoice.status
invoice.status = new_status
invoice.save()
# Log mesajı
print(f"Fatura {invoice_id} durumu '{old_status}' -> '{new_status}' olarak güncellendi")
return JsonResponse({
'success': True,
'message': f'Fatura durumu başarıyla güncellendi',
'old_status': old_status,
'new_status': new_status
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'message': 'Geçersiz JSON verisi'
}, status=400)
except Exception as e:
print(f"Fatura durum güncelleme hatası: {str(e)}")
return JsonResponse({
'success': False,
'message': f'Durum güncellenirken hata oluştu: {str(e)}'
}, status=500)
@login_required
@require_http_methods(["POST"])
def bulk_update_invoice_status(request):
"""Toplu fatura durum güncelleme"""
try:
data = json.loads(request.body)
invoice_ids = data.get('invoice_ids', [])
new_status = data.get('status')
if not invoice_ids or not new_status:
return JsonResponse({
'success': False,
'message': 'Fatura ID\'leri ve durum bilgisi gerekli'
}, status=400)
# Geçerli durum kontrolü
valid_statuses = ['draft', 'sent', 'paid', 'overdue', 'cancelled']
if new_status not in valid_statuses:
return JsonResponse({
'success': False,
'message': f'Geçersiz durum: {new_status}'
}, status=400)
# Faturaları toplu güncelle
updated_count = Invoice.objects.filter(id__in=invoice_ids).update(status=new_status)
print(f"{updated_count} fatura durumu '{new_status}' olarak güncellendi")
return JsonResponse({
'success': True,
'message': f'{updated_count} fatura durumu başarıyla güncellendi',
'updated_count': updated_count,
'new_status': new_status
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'message': 'Geçersiz JSON verisi'
}, status=400)
except Exception as e:
print(f"Toplu durum güncelleme hatası: {str(e)}")
return JsonResponse({
'success': False,
'message': f'Durumlar güncellenirken hata oluştu: {str(e)}'
}, status=500)
@login_required
@require_http_methods(["DELETE"])
def bulk_delete_invoices(request):
"""Toplu fatura silme"""
try:
data = json.loads(request.body)
invoice_ids = data.get('invoice_ids', [])
if not invoice_ids:
return JsonResponse({
'success': False,
'message': 'Fatura ID\'leri gerekli'
}, status=400)
# Faturaları toplu sil
deleted_count, _ = Invoice.objects.filter(id__in=invoice_ids).delete()
print(f"{deleted_count} fatura silindi")
return JsonResponse({
'success': True,
'message': f'{deleted_count} fatura başarıyla silindi',
'deleted_count': deleted_count
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'message': 'Geçersiz JSON verisi'
}, status=400)
except Exception as e:
print(f"Toplu silme hatası: {str(e)}")
return JsonResponse({
'success': False,
'message': f'Faturalar silinirken hata oluştu: {str(e)}'
}, status=500)
@login_required
def profit_loss_report(request):
"""Kar/Zarar raporu"""
# Tarih filtreleri
today = timezone.now().date()
start_date = request.GET.get('start_date', (today - timedelta(days=30)).strftime('%Y-%m-%d'))
end_date = request.GET.get('end_date', today.strftime('%Y-%m-%d'))
customer_id = request.GET.get('customer_id', '')
# String tarihleri datetime'a çevir
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
except ValueError:
start_date = today - timedelta(days=30)
end_date = today
# Temel sorgu
invoices = Invoice.objects.filter(
issue_date__range=[start_date, end_date],
status='paid' # Sadece ödenmiş faturalar
)
# Müşteri filtresi
if customer_id:
try:
customer = Customer.objects.get(id=customer_id)
invoices = invoices.filter(customer=customer)
except Customer.DoesNotExist:
customer = None
else:
customer = None
# Gelir ve gider hesaplamaları
income_invoices = invoices.filter(invoice_type='income')
expense_invoices = invoices.filter(invoice_type='expense')
total_income = income_invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0
total_expense = expense_invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0
net_profit = total_income - total_expense
# Genel özet
summary = {
'total_income': total_income,
'total_expense': total_expense,
'net_profit': net_profit,
'profit_margin': (net_profit / total_income * 100) if total_income > 0 else 0,
'income_count': income_invoices.count(),
'expense_count': expense_invoices.count(),
}
# Müşteri bazında kar/zarar (sadece genel rapor için)
customer_stats = []
if not customer_id:
customers = Customer.objects.filter(
invoices__issue_date__range=[start_date, end_date],
invoices__status='paid'
).distinct()
for cust in customers:
cust_invoices = invoices.filter(customer=cust)
cust_income = cust_invoices.filter(invoice_type='income').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
cust_expense = cust_invoices.filter(invoice_type='expense').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
cust_profit = cust_income - cust_expense
customer_stats.append({
'customer': cust,
'income': cust_income,
'expense': cust_expense,
'profit': cust_profit,
'profit_margin': (cust_profit / cust_income * 100) if cust_income > 0 else 0,
})
# Kar'a göre sırala
customer_stats.sort(key=lambda x: x['profit'], reverse=True)
# Aylık trend analizi
monthly_data = []
current_date = start_date.replace(day=1) # Ay başına ayarla
while current_date <= end_date:
month_end = current_date.replace(day=28) + timedelta(days=4) # Sonraki ay
month_end = month_end - timedelta(days=month_end.day) # Ayın son günü
month_invoices = invoices.filter(
issue_date__range=[current_date, month_end]
)
month_income = month_invoices.filter(invoice_type='income').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
month_expense = month_invoices.filter(invoice_type='expense').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
monthly_data.append({
'month': current_date.strftime('%Y-%m'),
'month_name': current_date.strftime('%B %Y'),
'income': month_income,
'expense': month_expense,
'profit': month_income - month_expense,
})
# Sonraki aya geç
if current_date.month == 12:
current_date = current_date.replace(year=current_date.year + 1, month=1)
else:
current_date = current_date.replace(month=current_date.month + 1)
# Son faturalar (gelir/gider ayrımı ile)
recent_income = income_invoices.order_by('-issue_date')[:5]
recent_expense = expense_invoices.order_by('-issue_date')[:5]
context = {
'summary': summary,
'customer_stats': customer_stats,
'monthly_data': monthly_data,
'recent_income': recent_income,
'recent_expense': recent_expense,
'start_date': start_date.strftime('%Y-%m-%d'),
'end_date': end_date.strftime('%Y-%m-%d'),
'today': today.strftime('%Y-%m-%d'),
'selected_customer': customer,
'customers': Customer.objects.filter(is_active=True).order_by('name'),
}
return render(request, 'ssh_manager/kar_zarar_raporu.html', context)

View File

@ -0,0 +1,57 @@
# Generated by Django 5.2.4 on 2025-07-24 19:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ssh_manager', '0012_sshcredential_connection_status_and_more'),
]
operations = [
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invoice_number', models.CharField(max_length=50, unique=True, verbose_name='Fatura No')),
('issue_date', models.DateField(verbose_name='Düzenleme Tarihi')),
('due_date', models.DateField(verbose_name='Son Ödeme Tarihi')),
('status', models.CharField(choices=[('draft', 'Taslak'), ('sent', 'Gönderildi'), ('paid', 'Ödendi'), ('overdue', 'Gecikti'), ('cancelled', 'İptal Edildi')], default='draft', max_length=20, verbose_name='Durum')),
('payment_method', models.CharField(choices=[('bank_transfer', 'Banka Havalesi'), ('credit_card', 'Kredi Kartı'), ('cash', 'Nakit'), ('other', 'Diğer')], default='bank_transfer', max_length=20, verbose_name='Ödeme Yöntemi')),
('subtotal', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Ara Toplam')),
('tax_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='KDV Tutarı')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Toplam Tutar')),
('discount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='İndirim')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')),
('payment_notes', models.TextField(blank=True, null=True, verbose_name='Ödeme Notları')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='ssh_manager.customer', verbose_name='Müşteri')),
],
options={
'verbose_name': 'Fatura',
'verbose_name_plural': 'Faturalar',
'ordering': ['-issue_date', '-id'],
},
),
migrations.CreateModel(
name='InvoiceItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.CharField(max_length=255, verbose_name='ıklama')),
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Miktar')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Birim Fiyat')),
('tax_rate', models.DecimalField(decimal_places=2, default=18, max_digits=5, verbose_name='KDV Oranı (%)')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tutar')),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='ssh_manager.invoice', verbose_name='Fatura')),
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ssh_manager.project', verbose_name='İlişkili Proje')),
],
options={
'verbose_name': 'Fatura Kalemi',
'verbose_name_plural': 'Fatura Kalemleri',
'ordering': ['id'],
},
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 5.2.4 on 2025-07-24 20:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ssh_manager', '0013_invoice_invoiceitem'),
]
operations = [
migrations.RemoveField(
model_name='invoice',
name='discount',
),
migrations.RemoveField(
model_name='invoice',
name='subtotal',
),
migrations.RemoveField(
model_name='invoice',
name='tax_amount',
),
migrations.RemoveField(
model_name='invoiceitem',
name='quantity',
),
migrations.RemoveField(
model_name='invoiceitem',
name='tax_rate',
),
migrations.RemoveField(
model_name='invoiceitem',
name='unit_price',
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 5.2.4 on 2025-07-26 03:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ssh_manager', '0014_remove_invoice_discount_remove_invoice_subtotal_and_more'),
]
operations = [
migrations.CreateModel(
name='Backup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('backup_type', models.CharField(choices=[('manual', 'Manuel'), ('auto', 'Otomatik'), ('scheduled', 'Zamanlanmış')], default='manual', max_length=20, verbose_name='Yedekleme Tipi')),
('status', models.CharField(choices=[('running', 'Devam Ediyor'), ('completed', 'Tamamlandı'), ('failed', 'Başarısız'), ('cancelled', 'İptal Edildi')], default='running', max_length=20, verbose_name='Durum')),
('start_time', models.DateTimeField(auto_now_add=True, verbose_name='Başlangıç Zamanı')),
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='Bitiş Zamanı')),
('file_path', models.CharField(blank=True, max_length=500, null=True, verbose_name='Dosya Yolu')),
('file_size', models.CharField(blank=True, max_length=50, null=True, verbose_name='Dosya Boyutu')),
('error_message', models.TextField(blank=True, null=True, verbose_name='Hata Mesajı')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.project', verbose_name='Proje')),
],
options={
'verbose_name': 'Yedekleme',
'verbose_name_plural': 'Yedeklemeler',
'ordering': ['-start_time'],
},
),
]

View File

@ -0,0 +1,49 @@
# Generated by Django 5.2.4 on 2025-07-26 08:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ssh_manager', '0015_backup'),
]
operations = [
migrations.CreateModel(
name='SystemSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('backup_enabled', models.BooleanField(default=False, verbose_name='Otomatik Yedekleme Aktif')),
('backup_frequency', models.CharField(choices=[('daily', 'Günlük'), ('weekly', 'Haftalık'), ('monthly', 'Aylık')], default='weekly', max_length=10, verbose_name='Yedekleme Sıklığı')),
('backup_hour', models.IntegerField(default=3, verbose_name='Yedekleme Saati (0-23)')),
('backup_minute', models.IntegerField(default=0, verbose_name='Yedekleme Dakikası (0-59)')),
('backup_day_of_week', models.IntegerField(blank=True, default=0, null=True, verbose_name='Haftanın Günü (0=Pazartesi, 6=Pazar)')),
('backup_day_of_month', models.IntegerField(blank=True, default=1, null=True, verbose_name='Ayın Günü (1-31)')),
('backup_retention_days', models.IntegerField(default=30, verbose_name='Yedekleri Saklama Süresi (Gün)')),
('backup_crontab_expression', models.CharField(blank=True, max_length=100, null=True, verbose_name='Crontab İfadesi')),
('backup_compression', models.BooleanField(default=True, verbose_name='Sıkıştırma Kullan')),
('backup_format', models.CharField(default='tar.gz', max_length=10, verbose_name='Yedekleme Format')),
('ssh_key_path', models.CharField(blank=True, max_length=255, null=True, verbose_name='SSH Anahtar Yolu')),
('ssh_key_passphrase', models.CharField(blank=True, max_length=255, null=True, verbose_name='SSH Anahtar Parolası')),
('email_notifications', models.BooleanField(default=False, verbose_name='E-posta Bildirimleri')),
('notification_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Bildirim E-postası')),
('backup_directory', models.CharField(default='/backups', max_length=255, verbose_name='Yedekleme Dizini')),
('use_s3_storage', models.BooleanField(default=False, verbose_name='S3 Depolama Kullan')),
('s3_access_key', models.CharField(blank=True, max_length=255, null=True, verbose_name='S3 Erişim Anahtarı')),
('s3_secret_key', models.CharField(blank=True, max_length=255, null=True, verbose_name='S3 Gizli Anahtar')),
('s3_bucket_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='S3 Bucket Adı')),
('s3_region', models.CharField(blank=True, max_length=50, null=True, verbose_name='S3 Bölge')),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Sistem Ayarları',
'verbose_name_plural': 'Sistem Ayarları',
},
),
migrations.AlterField(
model_name='backup',
name='file_size',
field=models.BigIntegerField(blank=True, null=True, verbose_name='Dosya Boyutu'),
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 5.2.4 on 2025-07-26 09:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ssh_manager', '0016_systemsettings_alter_backup_file_size'),
]
operations = [
migrations.RemoveField(
model_name='systemsettings',
name='backup_directory',
),
migrations.RemoveField(
model_name='systemsettings',
name='ssh_key_passphrase',
),
migrations.RemoveField(
model_name='systemsettings',
name='ssh_key_path',
),
migrations.AddField(
model_name='systemsettings',
name='s3_endpoint',
field=models.CharField(default='ams1.vultrobjects.com', max_length=255, verbose_name='Vultr S3 Endpoint'),
),
migrations.AlterField(
model_name='systemsettings',
name='s3_access_key',
field=models.CharField(default='', max_length=255, verbose_name='Vultr S3 Erişim Anahtarı'),
),
migrations.AlterField(
model_name='systemsettings',
name='s3_bucket_name',
field=models.CharField(default='backups', max_length=255, verbose_name='Vultr S3 Bucket Adı'),
),
migrations.AlterField(
model_name='systemsettings',
name='s3_region',
field=models.CharField(default='ams', max_length=50, verbose_name='Vultr S3 Bölge'),
),
migrations.AlterField(
model_name='systemsettings',
name='s3_secret_key',
field=models.CharField(default='', max_length=255, verbose_name='Vultr S3 Gizli Anahtar'),
),
migrations.AlterField(
model_name='systemsettings',
name='use_s3_storage',
field=models.BooleanField(default=True, help_text='Tüm yedekler Vultr S3 depolama alanına aktarılır', verbose_name='Vultr S3 Depolama'),
),
]

View File

@ -3,6 +3,7 @@ import logging
from .ssh_client import SSHManager
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from .system_settings import SystemSettings
logger = logging.getLogger(__name__)
@ -104,9 +105,10 @@ class Project(models.Model):
folder_name = models.CharField(max_length=100, verbose_name='Klasör Adı')
ssh_credential = models.ForeignKey(SSHCredential, on_delete=models.CASCADE)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, verbose_name='Müşteri', null=True, blank=True)
url = models.CharField(max_length=255, null=True, blank=True)
url = models.TextField(null=True, blank=True, verbose_name='Site URL')
disk_usage = models.CharField(max_length=20, null=True, blank=True)
last_backup = models.DateTimeField(null=True, blank=True, verbose_name='Son Yedekleme')
host_renewal_date = models.DateField(null=True, blank=True, verbose_name='Host Yenileme Tarihi')
meta_key = models.CharField(max_length=32, null=True, blank=True, verbose_name='Meta Key', help_text='Site aktiflik kontrolü için benzersiz anahtar')
is_site_active = models.BooleanField(default=False, verbose_name='Site Aktif')
last_site_check = models.DateTimeField(null=True, blank=True, verbose_name='Son Site Kontrolü')
@ -124,19 +126,21 @@ class Project(models.Model):
if not self.meta_key:
self.generate_meta_key()
self.save()
return f'<meta name="site-verification" content="{self.meta_key}">'
def clean(self):
# URL formatını kontrol et
if self.url:
# URL'den http/https ve www. kısımlarını temizle
cleaned_url = self.url.lower()
for prefix in ['http://', 'https://', 'www.']:
if cleaned_url.startswith(prefix):
cleaned_url = cleaned_url[len(prefix):]
# Sondaki / işaretini kaldır
cleaned_url = cleaned_url.rstrip('/')
self.url = cleaned_url
# Farklı meta tag formatları
meta_tags = [
f'<meta name="site-verification" content="{self.meta_key}">',
f'<meta name="site-verify" content="{self.meta_key}">',
f'<meta name="verification" content="{self.meta_key}">'
]
# HTML yorum içinde meta key
comment_tag = f'<!-- site-verification: {self.meta_key} -->'
# Tüm formatları birleştir
return meta_tags[0], meta_tags, comment_tag
def save(self, *args, **kwargs):
self.clean()
@ -175,6 +179,131 @@ class Project(models.Model):
verbose_name_plural = "Projeler"
ordering = ['-created_at']
class Invoice(models.Model):
INVOICE_STATUS = (
('draft', 'Taslak'),
('sent', 'Gönderildi'),
('paid', 'Ödendi'),
('overdue', 'Gecikti'),
('cancelled', 'İptal Edildi'),
)
PAYMENT_METHODS = (
('bank_transfer', 'Banka Havalesi'),
('credit_card', 'Kredi Kartı'),
('cash', 'Nakit'),
('other', 'Diğer'),
)
INVOICE_TYPES = (
('income', 'Gelir'),
('expense', 'Gider'),
)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='invoices', verbose_name='Müşteri')
invoice_number = models.CharField(max_length=50, unique=True, verbose_name='Fatura No')
invoice_type = models.CharField(max_length=10, choices=INVOICE_TYPES, default='income', verbose_name='Fatura Tipi')
issue_date = models.DateField(verbose_name='Düzenleme Tarihi')
due_date = models.DateField(verbose_name='Son Ödeme Tarihi')
status = models.CharField(max_length=20, choices=INVOICE_STATUS, default='draft', verbose_name='Durum')
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHODS, default='bank_transfer', verbose_name='Ödeme Yöntemi')
total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='Toplam Tutar')
notes = models.TextField(blank=True, null=True, verbose_name='Notlar')
payment_notes = models.TextField(blank=True, null=True, verbose_name='Ödeme Notları')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')
def __str__(self):
return f"Fatura #{self.invoice_number} - {self.customer.get_display_name()}"
def save(self, *args, **kwargs):
if not self.invoice_number:
# Otomatik fatura numarası oluştur (F-YIL-AYGUN-XXXX)
from datetime import datetime
year = datetime.now().strftime('%Y')
day_month = datetime.now().strftime('%d%m')
# Son fatura numarasını bul
last_invoice = Invoice.objects.filter(
invoice_number__startswith=f'F-{year}'
).order_by('-invoice_number').first()
if last_invoice:
try:
last_num = int(last_invoice.invoice_number.split('-')[-1])
new_num = last_num + 1
except (ValueError, IndexError):
new_num = 1
else:
new_num = 1
self.invoice_number = f'F-{year}-{day_month}-{new_num:04d}'
super().save(*args, **kwargs)
class Meta:
verbose_name = "Fatura"
verbose_name_plural = "Faturalar"
ordering = ['-issue_date', '-id']
class InvoiceItem(models.Model):
invoice = models.ForeignKey(Invoice, related_name='items', on_delete=models.CASCADE, verbose_name='Fatura')
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='İlişkili Proje')
description = models.CharField(max_length=255, verbose_name='ıklama')
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Tutar')
def __str__(self):
return f"{self.description} - {self.invoice.invoice_number}"
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.update_invoice_total()
def delete(self, *args, **kwargs):
invoice = self.invoice
super().delete(*args, **kwargs)
# Fatura silindiğinde tutarı güncelle
self.update_invoice_total()
def update_invoice_total(self):
"""Fatura toplam tutarını güncelle"""
from decimal import Decimal, InvalidOperation
invoice = self.invoice
items = invoice.items.all()
# Toplam tutar - güvenli dönüşümle hesapla
total = Decimal('0.00')
for item in items:
try:
# Eğer item.amount None ise veya geçersizse, 0 olarak kabul et
if item.amount is None:
continue
# String ise decimal'e çevir
if isinstance(item.amount, str):
if ',' in item.amount:
item.amount = item.amount.replace(',', '.')
item_amount = Decimal(item.amount)
else:
item_amount = Decimal(str(item.amount))
total += item_amount
except (InvalidOperation, ValueError, TypeError) as e:
print(f"Invoice total hesaplama hatası: {str(e)}")
# Hatalı öğeyi atla
continue
# Fatura tutarını güncelle
invoice.total_amount = total
invoice.save()
class Meta:
verbose_name = "Fatura Kalemi"
verbose_name_plural = "Fatura Kalemleri"
ordering = ['id']
class SSHLog(models.Model):
LOG_TYPES = (
('connection', 'Bağlantı Kontrolü'),
@ -196,3 +325,64 @@ class SSHLog(models.Model):
class Meta:
ordering = ['-created_at']
class Backup(models.Model):
BACKUP_STATUS = (
('running', 'Devam Ediyor'),
('completed', 'Tamamlandı'),
('failed', 'Başarısız'),
('cancelled', 'İptal Edildi'),
)
BACKUP_TYPE = (
('manual', 'Manuel'),
('auto', 'Otomatik'),
('scheduled', 'Zamanlanmış'),
)
project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='Proje')
backup_type = models.CharField(max_length=20, choices=BACKUP_TYPE, default='manual', verbose_name='Yedekleme Tipi')
status = models.CharField(max_length=20, choices=BACKUP_STATUS, default='running', verbose_name='Durum')
start_time = models.DateTimeField(auto_now_add=True, verbose_name='Başlangıç Zamanı')
end_time = models.DateTimeField(null=True, blank=True, verbose_name='Bitiş Zamanı')
file_path = models.CharField(max_length=500, null=True, blank=True, verbose_name='Dosya Yolu')
file_size = models.BigIntegerField(null=True, blank=True, verbose_name='Dosya Boyutu') # BigIntegerField kullanarak büyük dosya boyutlarını destekle
error_message = models.TextField(null=True, blank=True, verbose_name='Hata Mesajı')
notes = models.TextField(null=True, blank=True, verbose_name='Notlar')
def __str__(self):
return f"{self.project.name} - {self.start_time.strftime('%d.%m.%Y %H:%M')}"
@property
def duration(self):
"""Yedekleme süresini hesapla"""
if self.end_time:
duration = self.end_time - self.start_time
total_seconds = int(duration.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours}s {minutes}d {seconds}s"
elif minutes > 0:
return f"{minutes}d {seconds}s"
else:
return f"{seconds}s"
return "-"
@property
def status_badge_class(self):
"""Durum badge CSS sınıfı"""
status_classes = {
'running': 'bg-info',
'completed': 'bg-success',
'failed': 'bg-danger',
'cancelled': 'bg-warning'
}
return status_classes.get(self.status, 'bg-secondary')
class Meta:
verbose_name = "Yedekleme"
verbose_name_plural = "Yedeklemeler"
ordering = ['-start_time']

View File

@ -16,21 +16,39 @@ class SSHManager:
def connect(self):
"""SSH bağlantısı kur"""
import traceback
try:
logger.info(f"SSH bağlantısı başlatılıyor: {self.ssh_credential.hostname}:{self.ssh_credential.port or 22}")
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Bağlantı parametrelerini logla
logger.info(f"Bağlantı parametreleri:")
logger.info(f" Host: {self.ssh_credential.hostname}")
logger.info(f" Port: {self.ssh_credential.port or 22}")
logger.info(f" Username: {self.ssh_credential.username}")
# SSH timeout değeri ekle
self.client.connect(
hostname=self.ssh_credential.hostname,
username=self.ssh_credential.username,
password=self.ssh_credential.password,
port=self.ssh_credential.port or 22,
look_for_keys=False,
allow_agent=False
allow_agent=False,
timeout=30 # Timeout değeri ekle
)
logger.info(f"SSH bağlantısı başarıyla kuruldu: {self.ssh_credential.hostname}")
return True
except Exception as e:
error_details = traceback.format_exc()
logger.error(f'SSH bağlantı hatası: {str(e)}')
return False
logger.error(f'SSH bağlantı hata detayları:\n{error_details}')
raise Exception(f"SSH bağlantısı kurulamadı: {str(e)}")
def close(self):
"""SSH bağlantısını kapat"""
@ -52,12 +70,33 @@ class SSHManager:
"""
SSH üzerinden komut çalıştır ve sonuçları döndür
"""
import traceback
try:
logger.info(f"SSH komutu çalıştırılıyor: {command}")
if not self.client:
logger.info("SSH istemcisi yok, yeniden bağlanılıyor...")
self.connect()
stdin, stdout, stderr = self.client.exec_command(command)
if not self.client:
raise Exception("SSH istemcisi oluşturulamadı")
logger.info("Komut çalıştırılıyor...")
stdin, stdout, stderr = self.client.exec_command(command, timeout=120) # Timeout ekle
logger.info("Komut çalıştırıldı, çıkış kodu bekleniyor...")
# Timeout ile çıkış kodu bekle
import select
channel = stdout.channel
status_ready = select.select([channel], [], [], 120) # 120 saniye timeout
if not status_ready[0]:
logger.error("Komut zaman aşımına uğradı!")
raise Exception("Komut zaman aşımına uğradı")
exit_status = stdout.channel.recv_exit_status()
logger.info(f"Komut çıkış kodu: {exit_status}")
# Binary veriyi oku
stdout_data = stdout.read()

View File

@ -0,0 +1,74 @@
from django.db import models
class SystemSettings(models.Model):
"""Sistem genel ayarları"""
BACKUP_FREQUENCY_CHOICES = [
('daily', 'Günlük'),
('weekly', 'Haftalık'),
('monthly', 'Aylık'),
]
# Yedekleme ayarları
backup_enabled = models.BooleanField(default=False, verbose_name="Otomatik Yedekleme Aktif")
backup_frequency = models.CharField(max_length=10, choices=BACKUP_FREQUENCY_CHOICES, default='weekly', verbose_name="Yedekleme Sıklığı")
backup_hour = models.IntegerField(default=3, verbose_name="Yedekleme Saati (0-23)")
backup_minute = models.IntegerField(default=0, verbose_name="Yedekleme Dakikası (0-59)")
backup_day_of_week = models.IntegerField(default=0, verbose_name="Haftanın Günü (0=Pazartesi, 6=Pazar)", null=True, blank=True)
backup_day_of_month = models.IntegerField(default=1, verbose_name="Ayın Günü (1-31)", null=True, blank=True)
# Linux crontab komutu
backup_crontab_expression = models.CharField(max_length=100, blank=True, null=True, verbose_name="Crontab İfadesi")
# Yedekleme dosya formatı - Her zaman sıkıştırılmış
backup_format = models.CharField(max_length=10, default='tar.gz', verbose_name="Yedekleme Format")
# E-posta bildirim ayarları
email_notifications = models.BooleanField(default=False, verbose_name="E-posta Bildirimleri")
notification_email = models.EmailField(blank=True, null=True, verbose_name="Bildirim E-postası")
# S3 depolama ayarları (Vultr Object Storage)
use_s3_storage = models.BooleanField(default=True, verbose_name="Vultr S3 Depolama", help_text="Tüm yedekler Vultr S3 depolama alanına aktarılır")
s3_access_key = models.CharField(max_length=255, default='', verbose_name="Vultr S3 Erişim Anahtarı")
s3_secret_key = models.CharField(max_length=255, default='', verbose_name="Vultr S3 Gizli Anahtar")
s3_bucket_name = models.CharField(max_length=255, default='backups', verbose_name="Vultr S3 Bucket Adı")
s3_region = models.CharField(max_length=50, default='ams', verbose_name="Vultr S3 Bölge")
s3_endpoint = models.CharField(max_length=255, default='ams1.vultrobjects.com', verbose_name="Vultr S3 Endpoint")
# Son güncelleme zamanı
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return "Sistem Ayarları"
def save(self, *args, **kwargs):
# Crontab ifadesini oluştur
minute = self.backup_minute
hour = self.backup_hour
if self.backup_frequency == 'daily':
day_of_month = '*'
month = '*'
day_of_week = '*'
elif self.backup_frequency == 'weekly':
day_of_month = '*'
month = '*'
day_of_week = self.backup_day_of_week
else: # monthly
day_of_month = self.backup_day_of_month
month = '*'
day_of_week = '*'
self.backup_crontab_expression = f"{minute} {hour} {day_of_month} {month} {day_of_week}"
# S3 yedekleme zorunlu olduğu için her zaman kontrol et
if not self.s3_access_key or not self.s3_secret_key or not self.s3_bucket_name:
raise ValueError("Vultr S3 Erişim Anahtarı, Gizli Anahtar ve Bucket Adı alanları zorunludur!")
# S3 storage her zaman aktif
self.use_s3_storage = True
super().save(*args, **kwargs)
class Meta:
verbose_name = "Sistem Ayarları"
verbose_name_plural = "Sistem Ayarları"

View File

@ -1,5 +1,6 @@
from django.urls import path
from . import views
from . import invoice_views
from django.apps import apps
from django.core.management import call_command
@ -29,11 +30,12 @@ urlpatterns = [
path('get_host/<int:host_id>/', views.get_host, name='get_host'),
path('update_host/<int:host_id>/', views.update_host, name='update_host'),
path('delete_host/<int:host_id>/', views.delete_host, name='delete_host'),
path('update-hosts-status/', views.update_hosts_status, name='update_hosts_status'),
path('update-hosts-status/', views.update_all_hosts_status, name='update_hosts_status'),
path('project/create/', views.create_project, name='create_project'),
path('project/<int:project_id>/upload/', views.upload_project_zip, name='upload_project_zip'),
path('delete_project/<int:project_id>/', views.delete_project, name='delete_project'),
path('project/<int:project_id>/setup-venv/', views.setup_venv, name='setup_venv'),
path('project/<int:project_id>/renew-host/', views.renew_host, name='renew_host'),
path('project/<int:project_id>/check-requirements/', views.check_requirements, name='check_requirements'),
path('project/<int:project_id>/update-requirements/', views.update_requirements, name='update_requirements'),
path('project/<int:project_id>/delete-requirement-line/', views.delete_requirement_line, name='delete_requirement_line'),
@ -57,7 +59,7 @@ urlpatterns = [
path('project/<int:project_id>/clear-logs/', views.clear_project_logs, name='clear_project_logs'),
path('project/<int:project_id>/check-site/', views.check_site_status_view, name='check_site_status'),
path('project/<int:project_id>/meta-key/', views.get_project_meta_key, name='get_project_meta_key'),
path('check-all-sites/', views.check_all_sites_view, name='check_all_sites'),
path('check-all-sites/', views.check_site_status_view, name='check_all_sites'),
path('get-project-details/<int:project_id>/', views.get_project_details, name='get_project_details'),
path('update-project/<int:project_id>/', views.update_project, name='update_project'),
@ -74,6 +76,23 @@ urlpatterns = [
path('start-backup/', views.start_backup, name='start_backup'),
path('backup-all-projects/', views.backup_all_projects, name='backup_all_projects'),
path('retry-backup/', views.retry_backup, name='retry_backup'),
path('backup-details/<int:backup_id>/', views.backup_details, name='backup_details'),
path('cancel-backup/<int:backup_id>/', views.cancel_backup, name='cancel_backup'),
path('delete-backup-record/<int:backup_id>/', views.delete_backup_record, name='delete_backup_record'),
# path('upload-to-drive/<int:project_id>/', views.upload_to_drive, name='upload_to_drive').
# Invoice URLs
path('faturalar/', invoice_views.invoices, name='faturalar'),
path('test-fatura/', invoice_views.test_fatura, name='test_fatura'),
path('faturalar/detay/<int:invoice_id>/', invoice_views.invoice_detail, name='invoice_detail'),
path('faturalar/create/', invoice_views.create_invoice, name='create_invoice'),
path('faturalar/update/<int:invoice_id>/', invoice_views.update_invoice, name='update_invoice'),
path('faturalar/delete/<int:invoice_id>/', invoice_views.delete_invoice, name='delete_invoice'),
path('faturalar/update-status/<int:invoice_id>/', invoice_views.update_invoice_status, name='update_invoice_status'),
path('faturalar/bulk-update-status/', invoice_views.bulk_update_invoice_status, name='bulk_update_invoice_status'),
path('faturalar/bulk-delete/', invoice_views.bulk_delete_invoices, name='bulk_delete_invoices'),
path('faturalar/detay-json/<int:invoice_id>/', invoice_views.get_invoice_details, name='get_invoice_details'),
path('faturalar/raporlar/', invoice_views.invoice_reports, name='invoice_reports'),
path('faturalar/kar-zarar/', invoice_views.profit_loss_report, name='profit_loss_report'),
path('musteri/<int:customer_id>/projeler/', invoice_views.get_projects_by_customer, name='get_projects_by_customer'),
path('check-domain-expiration/', views.get_domain_expiration, name='check_domain_expiration'), # Domain sorgulama endpoint'i
]

View File

@ -10,21 +10,23 @@ logger = logging.getLogger(__name__)
# Yardımcı fonksiyonlar buraya gelebilir
def check_site_status(project):
def check_disk_usage(project):
"""
Projenin web sitesinin aktif olup olmadığını kontrol eder
Aynı zamanda proje klasörünün disk kullanımını günceller
Proje klasörünün disk kullanımını kontrol eder ve günceller
"""
from .ssh_client import SSHManager
result_messages = []
# 1. Disk kullanımını güncelle
try:
if project.ssh_credential:
if not project.ssh_credential:
return False, "SSH bilgisi eksik"
ssh_manager = SSHManager(project.ssh_credential)
if ssh_manager.check_connection():
if not ssh_manager.check_connection():
return False, "SSH bağlantısı kurulamadı"
# Proje klasörünün tam yolu
base_path = project.ssh_credential.base_path.rstrip('/')
folder_name = project.folder_name.strip('/')
@ -37,7 +39,10 @@ def check_site_status(project):
base_check_command = f"test -d '{base_path}' && echo 'BASE_EXISTS' || echo 'BASE_NOT_EXISTS'"
stdout_base, stderr_base, success_base = ssh_manager.execute_command(base_check_command)
if success_base and stdout_base.strip() == 'BASE_EXISTS':
if not (success_base and stdout_base.strip() == 'BASE_EXISTS'):
ssh_manager.close()
return False, f"Base path bulunamadı: {base_path}"
# Base path var, şimdi proje klasörünü kontrol et
# Önce base path içindeki klasörleri listele
@ -51,7 +56,10 @@ def check_site_status(project):
check_command = f"test -d '{full_path}' && echo 'EXISTS' || echo 'NOT_EXISTS'"
stdout_check, stderr_check, success_check = ssh_manager.execute_command(check_command)
if success_check and stdout_check.strip() == 'EXISTS':
if not (success_check and stdout_check.strip() == 'EXISTS'):
ssh_manager.close()
return False, f"Proje klasörü bulunamadı: {full_path}"
# Disk kullanımını al
command = f"du -sh '{full_path}' 2>/dev/null | cut -f1"
stdout, stderr, success = ssh_manager.execute_command(command)
@ -59,27 +67,38 @@ def check_site_status(project):
if success and stdout.strip():
old_usage = project.disk_usage or "Bilinmiyor"
project.disk_usage = stdout.strip()
project.save() # Veritabanına kaydet
result_messages.append(f"Disk kullanımı güncellendi: {old_usage}{project.disk_usage}")
else:
result_messages.append("Disk kullanımı komutu başarısız")
else:
result_messages.append(f"Proje klasörü bulunamadı: {full_path}")
else:
result_messages.append(f"Base path bulunamadı: {base_path}")
ssh_manager.close()
return True, "; ".join(result_messages)
else:
result_messages.append("SSH bağlantısı kurulamadı")
else:
result_messages.append("SSH bilgisi eksik")
except Exception as e:
result_messages.append(f"Disk kontrolü hatası: {str(e)}")
ssh_manager.close()
return False, "Disk kullanımı komutu başarısız"
# 2. Site durumunu kontrol et
except Exception as e:
if 'ssh_manager' in locals() and ssh_manager:
ssh_manager.close()
return False, f"Disk kontrolü hatası: {str(e)}"
def check_site_status(project):
"""
Projenin web sitesinin aktif olup olmadığını kontrol eder
SADECE meta key doğrulaması ile site aktifliği belirlenir
"""
result_messages = []
# Site durumunu kontrol et
if not project.url:
project.last_site_check = timezone.now()
project.save()
return False, "; ".join(result_messages + ["URL eksik"])
return False, "URL eksik"
# Meta key kontrolü zorunlu
if not project.meta_key:
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
return False, "Meta key tanımlanmamış - Site doğrulaması için meta key gerekli"
try:
# URL'yi düzenle
@ -87,7 +106,7 @@ def check_site_status(project):
if not url.startswith(('http://', 'https://')):
url = f'http://{url}'
# Site kontrolü
# Site kontrolü - sadece erişilebilirlik için
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
@ -95,40 +114,107 @@ def check_site_status(project):
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
# Site erişilebilir
# Site erişilebilir, şimdi ZORUNLU meta key kontrolü yapalım
try:
# HTML içeriğini parse et
soup = BeautifulSoup(response.content, 'html.parser')
# Meta tag'i farklı formatlarda ara
meta_tags = []
meta_tags.append(soup.find('meta', attrs={'name': 'site-verification'}))
meta_tags.append(soup.find('meta', attrs={'name': 'site-verify'}))
meta_tags.append(soup.find('meta', attrs={'name': 'verification'}))
# HTML içeriğinde meta key'i doğrudan ara
html_content = response.text
meta_key_in_html = project.meta_key in html_content
# Debug bilgisi ekle
result_messages.append(f"HTML içeriğinde meta key aranıyor: {project.meta_key}")
result_messages.append(f"HTML içeriğinde meta key bulundu: {meta_key_in_html}")
# Bulunan meta tag'leri kontrol et
found_meta_tag = None
for i, tag in enumerate(meta_tags):
if tag:
result_messages.append(f"Meta tag bulundu: {tag}")
if tag.get('content') and project.meta_key in tag.get('content'):
found_meta_tag = tag
result_messages.append(f"Meta key, meta tag içeriğinde doğrulandı")
break
# Tüm meta tag'lerde içerik kontrolü yap
if not found_meta_tag:
all_meta_tags = soup.find_all('meta')
for tag in all_meta_tags:
content = tag.get('content')
if content and project.meta_key in content:
found_meta_tag = tag
result_messages.append(f"Meta key, '{tag.get('name', 'isimsiz')}' meta tag'de bulundu")
break
# HTML yorum kontrolü (<!-- site-verification: key --> formatı için)
comment_pattern = f'<!-- site-verification: {project.meta_key} -->'
comment_found = comment_pattern in html_content
if comment_found:
result_messages.append("Meta key HTML yorum olarak bulundu")
# Meta key doğrulaması - ZORUNLU
if found_meta_tag or meta_key_in_html or comment_found:
# Meta key doğrulandı - Site AKTIF
project.is_site_active = True
project.last_site_check = timezone.now()
project.save()
result_messages.append(f"Site aktif (HTTP {response.status_code})")
return True, "; ".join(result_messages)
if found_meta_tag:
return True, f"✅ Site AKTIF - Meta key meta tag'de doğrulandı"
elif comment_found:
return True, f"✅ Site AKTIF - Meta key HTML yorumunda doğrulandı"
else:
# Site erişilemez
return True, f"✅ Site AKTIF - Meta key HTML içeriğinde doğrulandı"
else:
# Meta key bulunamadı - Site PASİF
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
result_messages.append(f"Site erişilemez (HTTP {response.status_code})")
return False, "; ".join(result_messages)
return False, f"❌ Site PASİF - Site erişilebilir (HTTP {response.status_code}) ancak meta key doğrulanamadı"
except Exception as e:
# Meta key kontrolünde hata - Site PASİF
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
return False, f"❌ Site PASİF - Meta key kontrolü hatası: {str(e)}"
else:
# Site erişilemez - Site PASİF
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
return False, f"❌ Site PASİF - Site erişilemez (HTTP {response.status_code})"
except requests.exceptions.Timeout:
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
result_messages.append("Site zaman aşımı")
return False, "; ".join(result_messages)
return False, "Site PASİF - Zaman aşımı hatası"
except requests.exceptions.ConnectionError:
except requests.exceptions.ConnectionError as e:
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
result_messages.append("Site bağlantı hatası")
return False, "; ".join(result_messages)
# DNS çözümleme hatası kontrolü
error_str = str(e)
if "NameResolutionError" in error_str or "getaddrinfo failed" in error_str:
return False, f"❌ Site PASİF - Domain '{project.url}' DNS kayıtlarında bulunamadı"
else:
return False, f"❌ Site PASİF - Bağlantı hatası: {str(e)}"
except Exception as e:
project.is_site_active = False
project.last_site_check = timezone.now()
project.save()
result_messages.append(f"Site hatası: {str(e)}")
return False, "; ".join(result_messages)
return False, f"Site PASİF - Genel hata: {str(e)}"
def check_all_sites():
"""Tüm projelerin site durumunu kontrol et"""
@ -136,11 +222,16 @@ def check_all_sites():
results = []
for project in projects:
status, message = check_site_status(project)
site_status, site_message = check_site_status(project)
disk_status, disk_message = check_disk_usage(project)
results.append({
'project': project,
'status': status,
'message': message
'status': site_status, # Site durumunu ana durum olarak kullan
'site_message': site_message,
'disk_status': disk_status,
'disk_message': disk_message,
'combined_message': f"Site: {site_message} | Disk: {disk_message}"
})
return results

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
<title>{% block title %}Hosting Yönetim Paneli{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
{% block head_extras %}{% endblock %}
<style>
body {
background: #181a1b;
@ -13,6 +14,13 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Custom alert styles */
.alert.alert-success.alert-dismissible.fade.show {
background-color: #333333 !important;
color: #ffffff;
border-color: #28a745;
}
/* Sidebar Styles */
.sidebar {
position: fixed;
@ -86,13 +94,10 @@
.nav-dropdown-content {
display: none;
position: absolute;
left: 0;
top: 100%;
position: relative;
width: 100%;
background: #1a1d23;
border-left: 3px solid #4fc3f7;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 1000;
}
@ -127,6 +132,10 @@
transition: transform 0.3s ease;
}
.nav-dropdown.active .nav-dropdown-toggle::after {
transform: rotate(180deg);
}
.nav-dropdown.active .nav-dropdown-toggle::after {
transform: rotate(90deg);
}
@ -344,10 +353,20 @@
<i class="bi bi-speedometer2"></i>
Dashboard
</a>
<a href="{% url 'musteriler' %}" class="nav-item {% if request.resolver_match.url_name == 'musteriler' %}active{% endif %}">
<div class="nav-dropdown {% if request.resolver_match.url_name == 'musteriler' or request.resolver_match.url_name == 'faturalar' %}active{% endif %}">
<a href="#" class="nav-item nav-dropdown-toggle {% if request.resolver_match.url_name == 'musteriler' or request.resolver_match.url_name == 'faturalar' %}active{% endif %}" onclick="toggleDropdown(this); return false;">
<i class="bi bi-people"></i>
Müşteriler
</a>
<div class="nav-dropdown-content">
<a href="{% url 'musteriler' %}" class="nav-dropdown-item {% if request.resolver_match.url_name == 'musteriler' %}active{% endif %}">
<i class="bi bi-person-lines-fill"></i> Müşteri Listesi
</a>
<a href="{% url 'faturalar' %}" class="nav-dropdown-item {% if request.resolver_match.url_name == 'faturalar' %}active{% endif %}">
<i class="bi bi-receipt"></i> Faturalar
</a>
</div>
</div>
<a href="{% url 'host_yonetimi' %}" class="nav-item {% if request.resolver_match.url_name == 'host_yonetimi' %}active{% endif %}">
<i class="bi bi-hdd-network"></i>
Host Yönetimi
@ -425,7 +444,7 @@ function showToast(message, type = 'info') {
const toast = document.getElementById('mainToast');
const toastBody = document.getElementById('mainToastBody');
toastBody.textContent = message;
toastBody.innerHTML = message;
// Remove existing classes
toast.classList.remove('text-bg-dark', 'text-bg-success', 'text-bg-danger', 'text-bg-warning', 'text-bg-info');
@ -502,5 +521,9 @@ document.addEventListener('click', function(event) {
}
});
</script>
<!-- EXTRA SCRIPTS BLOCK -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -371,7 +371,7 @@ function refreshAllHosts() {
// Tüm siteleri kontrol et
function checkAllSites() {
showToast('Tüm siteler kontrol ediliyor...', 'info');
showToast('Tüm siteler ve disk kullanımları kontrol ediliyor...', 'info');
fetch('/check-all-sites/', {
method: 'POST',
@ -383,14 +383,27 @@ function checkAllSites() {
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message, 'success');
// Sonuçları göster
const activeCount = data.results.filter(r => r.status).length;
const totalCount = data.results.length;
// Özet mesaj oluştur
const summaryMessage = `
<div>
<div><strong>Site Kontrolü Tamamlandı</strong></div>
<div class="mt-2">${activeCount}/${totalCount} site aktif</div>
<div class="small text-muted mt-1">Disk kullanımları güncellendi</div>
</div>
`;
showToast(summaryMessage, 'success', 5000);
setTimeout(() => location.reload(), 2000);
} else {
showToast(data.message, 'error');
}
})
.catch(error => {
showToast('Site kontrol hatası', 'error');
showToast('Site ve disk kontrolü hatası', 'error');
});
}

View File

@ -3,6 +3,24 @@
{% block content %}
<style>
/* Alt menü stili */
.nav-pills .nav-link {
color: #e0e0e0;
border-radius: 5px;
padding: 0.5rem 1rem;
transition: all 0.2s ease;
}
.nav-pills .nav-link:hover {
background-color: rgba(79, 195, 247, 0.1);
}
.nav-pills .nav-link.active {
background-color: #4fc3f7;
color: #212529;
}
/* Müşteri kartları */
.customer-card {
background: #23272b;
border: 1px solid #333;
@ -31,6 +49,22 @@
}
</style>
<!-- Alt Menü -->
<div class="mb-4 border-bottom pb-2">
<ul class="nav nav-pills">
<li class="nav-item">
<a href="{% url 'musteriler' %}" class="nav-link active">
<i class="bi bi-person-lines-fill"></i> Müşteri Listesi
</a>
</li>
<li class="nav-item">
<a href="{% url 'faturalar' %}" class="nav-link text-light">
<i class="bi bi-receipt"></i> Faturalar
</a>
</li>
</ul>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3>Müşteri Yönetimi

View File

@ -442,16 +442,23 @@
<!-- Meta Key Modal -->
<div class="modal fade" id="metaKeyModal" tabindex="-1" aria-labelledby="metaKeyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="metaKeyModalLabel">Site Doğrulama Meta Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
<div class="modal-content bg-dark text-light">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="metaKeyModalLabel">
<i class="bi bi-key me-2"></i>Site Doğrulama Meta Key
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Kapat"></button>
</div>
<div class="modal-body" id="metaKeyContent">
Yükleniyor...
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Yükleniyor...</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
<p class="mt-2">Meta key bilgileri yükleniyor...</p>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Kapat</button>
</div>
</div>
</div>
@ -515,7 +522,7 @@ function deleteHost(id) {
headers: { 'X-CSRFToken': getCookie('csrftoken') }
})
.then(r => r.json())
.then(data => {
.then data => {
toastMessage(data.message);
if (data.success) setTimeout(() => location.reload(), 1200);
});
@ -525,7 +532,7 @@ function deleteHost(id) {
// Host Düzenle
function editHost(id) {
fetch(`/get_host/${id}/`)
.then(r => r.json())
.then r => r.json())
.then(data => {
if (data.success) {
document.getElementById('hostId').value = data.host.id;
@ -607,7 +614,7 @@ window.deleteProject = function(id) {
headers: { 'X-CSRFToken': getCookie('csrftoken') }
})
.then(r => r.json())
.then(data => {
.then data => {
toastMessage(data.message);
if (data.success) {
setTimeout(() => location.reload(), 1200);
@ -630,7 +637,7 @@ window.backupProject = function(id) {
updateProgress(40, 'İşlem devam ediyor...');
return r.json();
})
.then(data => {
.then data => {
updateProgress(80, 'Tamamlanıyor...');
setTimeout(() => {
@ -711,6 +718,12 @@ window.showLogsByProject = function(projectId) {
});
}
// Verify site function - global scope
window.verifySite = function(projectId) {
checkSiteStatus(projectId);
$('#metaKeyModal').modal('hide');
}
// Site durumu kontrol fonksiyonu - global scope
window.checkSiteStatus = function(projectId) {
// Progress toast'ı başlat
@ -737,13 +750,30 @@ window.checkSiteStatus = function(projectId) {
hideProgressToast();
if (data.success) {
const statusText = data.status ? '✅ Site Aktif' : '❌ Site Pasif';
showToast(statusText, data.status ? 'success' : 'error');
const siteStatusText = data.status ? '✅ Site Aktif' : '❌ Site Pasif';
const diskStatusText = data.disk_message ? `💾 ${data.disk_message}` : '';
// Detaylı bilgi içeren bir toast göster
const statusHtml = `
<div>
<div><strong>${siteStatusText}</strong></div>
<div class="small text-light-emphasis">${data.site_message}</div>
${diskStatusText ? `<div class="mt-2"><strong>${diskStatusText}</strong></div>` : ''}
</div>
`;
showToast(statusHtml, data.status ? 'success' : 'error', 8000);
// Sayfayı yenile
setTimeout(() => location.reload(), 1500);
} else {
showToast(`${data.message}`, 'error');
const errorHtml = `
<div>
<div><strong>❌ Kontrol Başarısız</strong></div>
<div class="small text-light-emphasis">${data.message}</div>
</div>
`;
showToast(errorHtml, 'error', 8000);
}
}, 300);
}, 500);
@ -751,7 +781,13 @@ window.checkSiteStatus = function(projectId) {
.catch(error => {
hideProgressToast();
console.error('Error:', error);
showToast('❌ Kontrol hatası!', 'error');
const errorHtml = `
<div>
<div><strong>❌ Kontrol Hatası</strong></div>
<div class="small text-light-emphasis">Sunucu ile iletişim sırasında bir hata oluştu.</div>
</div>
`;
showToast(errorHtml, 'error', 5000);
});
}
@ -762,33 +798,47 @@ window.showMetaKey = function(projectId) {
.then(data => {
if (data.success) {
const content = `
<div class="alert alert-info">
<h6><i class="bi bi-info-circle"></i> Kullanım Talimatları:</h6>
<p>Bu meta tag'ı sitenizin <code>&lt;head&gt;</code> bölümüne ekleyin:</p>
<div class="alert alert-info bg-info bg-opacity-25 text-info mb-3">
<h6><i class="bi bi-info-circle"></i> Site Doğrulama</h6>
<p>Site sahipliğinizi doğrulamak için aşağıdaki iki yöntemden birini kullanın:</p>
</div>
<div class="mb-3">
<label class="form-label"><strong>Meta Key:</strong></label>
<div class="card mb-3 bg-dark border-secondary">
<div class="card-header bg-dark border-secondary">
<strong>1. Yöntem: HTML Meta Tag</strong> <span class="badge bg-success">Önerilen</span>
</div>
<div class="card-body bg-dark text-light">
<div class="input-group">
<input type="text" class="form-control" value="${data.meta_key}" readonly>
<button class="btn btn-outline-secondary" onclick="copyMetaKey('${data.meta_key}')">
<i class="bi bi-clipboard"></i>
<textarea class="form-control bg-dark text-light border-secondary" rows="2" readonly id="metaTagText">${data.meta_tag}</textarea>
<button class="btn btn-outline-primary" onclick="copyMetaTag()">
<i class="bi bi-clipboard"></i> Kopyala
</button>
</div>
<small class="text-light-emphasis mt-2 d-block">Bu meta tag'ı sitenizin <code class="bg-dark text-light">&lt;head&gt;</code> bölümüne ekleyin</small>
</div>
</div>
<div class="mb-3">
<label class="form-label"><strong>HTML Meta Tag:</strong></label>
<div class="card mb-3 bg-dark border-secondary">
<div class="card-header bg-dark border-secondary">
<strong>2. Yöntem: HTML Yorum</strong>
</div>
<div class="card-body bg-dark text-light">
<div class="input-group">
<textarea class="form-control" rows="2" readonly id="metaTagText">${data.meta_tag}</textarea>
<button class="btn btn-outline-secondary" onclick="copyMetaTag()">
<i class="bi bi-clipboard"></i>
<textarea class="form-control bg-dark text-light border-secondary" rows="2" readonly>${data.comment_tag}</textarea>
<button class="btn btn-outline-primary" onclick="copyToClipboard('${data.comment_tag}')">
<i class="bi bi-clipboard"></i> Kopyala
</button>
</div>
<small class="text-light-emphasis mt-2 d-block">Bu yorum satırını HTML sayfanızın herhangi bir yerine ekleyebilirsiniz</small>
</div>
</div>
<div class="alert alert-warning">
<small><i class="bi bi-exclamation-triangle"></i> Meta tag'ı ekledikten sonra "Site Kontrol" butonuyla doğrulama yapabilirsiniz.</small>
<div class="alert alert-warning bg-warning bg-opacity-25 text-warning">
<i class="bi bi-exclamation-triangle"></i> Yukarıdaki yöntemlerden birini uyguladıktan sonra
<button type="button" class="btn btn-sm btn-outline-light" onclick="verifySite(${data.project_id})">
<i class="bi bi-check-circle"></i> Site Kontrol
</button>
butonuyla doğrulama yapabilirsiniz.
</div>
`;
@ -924,7 +974,7 @@ if (projectForm) {
body: formData
})
.then(r => r.json())
.then(data => {
.then data => {
toastMessage(data.message);
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('addProjectModal'));
@ -970,7 +1020,7 @@ if (hostForm) {
body: formData
})
.then(r => r.json())
.then(data => {
.then data => {
toastMessage(data.message);
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('addHostModal'));
@ -1051,7 +1101,7 @@ if (refreshHostsBtn) {
console.log('Response status:', response.status);
return response.json();
})
.then(data => {
.then data => {
console.log('Response data:', data);
hideProgressToast();
@ -1074,7 +1124,6 @@ if (refreshHostsBtn) {
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM yüklendi');
});
</script>
</div> <!-- main-content kapanış -->
</body>

View File

@ -12,9 +12,16 @@
<small class="text-muted">Tüm hosting projeleri ve yönetim işlemleri</small>
</div>
<div class="d-flex gap-2">
<select id="statusFilter" class="form-select" style="max-width: 180px;">
<option value="">Tüm Projeler</option>
<option value="active">🟢 Aktif Projeler</option>
<option value="inactive">🔴 Pasif Projeler</option>
<option value="unknown">⚫ Bilinmeyen Durumlar</option>
<option value="no-url">🔘 URL'siz Projeler</option>
</select>
<input type="text" id="projectSearch" class="form-control" style="max-width: 250px;" placeholder="Proje ara...">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal">
<i class="bi bi-plus-circle"></i> Yeni Proje
<i class="bi bi-plus-circle"></i>
</button>
</div>
</div>
@ -28,6 +35,7 @@
<th>Proje Bilgileri</th>
<th>Klasör & Disk</th>
<th>Site Durumu</th>
<th>Host Yenileme</th>
<th>Son Yedekleme</th>
<th class="actions">İşlemler</th>
</tr>
@ -85,6 +93,21 @@
<span class="text-muted">URL tanımlanmamış</span>
{% endif %}
</td>
<td>
{% if project.host_renewal_date %}
{% if project.host_renewal_date <= today %}
<small class="text-danger fw-bold">{{ project.host_renewal_date|date:"d.m.Y" }}</small>
<br><span class="badge bg-danger">Süresi Dolmuş</span>
{% elif project.host_renewal_date <= warning_date %}
<small>{{ project.host_renewal_date|date:"d.m.Y" }}</small>
<br><span class="badge bg-warning">Yaklaşıyor</span>
{% else %}
<small>{{ project.host_renewal_date|date:"d.m.Y" }}</small>
{% endif %}
{% else %}
<span class="text-muted">Tarih yok</span>
{% endif %}
</td>
<td>
{% if project.last_backup %}
<small>{{ project.last_backup|date:"d.m.Y H:i" }}</small>
@ -93,13 +116,14 @@
{% endif %}
</td>
<td class="actions">
<i class="action-icon edit bi bi-pencil" onclick="editProject({{ project.id }})" title="Düzenle"></i>
<i class="action-icon delete bi bi-trash" onclick="deleteProject({{ project.id }})" title="Sil"></i>
<i class="action-icon backup bi bi-cloud-arrow-up" onclick="backupProject({{ project.id }})" title="Yedekle"></i>
<i class="action-icon logs bi bi-journal-text" onclick="showLogsByProject({{ project.id }})" title="Loglar"></i>
<i class="action-icon edit bi bi-pencil" onclick="ProjectManager.editProject({{ project.id }})" title="Düzenle"></i>
<i class="action-icon delete bi bi-trash" onclick="ProjectManager.deleteProject({{ project.id }})" title="Sil"></i>
<i class="action-icon renewal bi bi-calendar-plus" onclick="ProjectManager.renewHost({{ project.id }})" title="Domain Expiration Tarihi Sorgula ve Host Yenile"></i>
<i class="action-icon backup bi bi-cloud-arrow-up" onclick="ProjectManager.backupProject({{ project.id }})" title="Yedekle"></i>
<i class="action-icon logs bi bi-journal-text" onclick="ProjectManager.showLogsByProject({{ project.id }})" title="Loglar"></i>
{% if project.url %}
<i class="action-icon site bi bi-globe-americas" onclick="checkSiteStatus({{ project.id }})" title="Site Kontrol"></i>
<i class="action-icon meta bi bi-key" onclick="showMetaKey({{ project.id }})" title="Meta Key"></i>
<i class="action-icon site bi bi-globe-americas" onclick="ProjectManager.checkSiteStatus({{ project.id }})" title="Site Kontrol"></i>
<i class="action-icon meta bi bi-key" onclick="ProjectManager.showMetaKey({{ project.id }})" title="Meta Key"></i>
{% endif %}
</td>
</tr>
@ -123,6 +147,7 @@
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<form id="projectForm">
<input type="hidden" id="projectId" name="id" value="">
<div class="modal-header">
<h5 class="modal-title" id="addProjectModalLabel">Yeni Proje Ekle</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
@ -171,11 +196,16 @@
<i class="bi bi-globe me-1"></i>Site Bilgileri
</h6>
<div class="row g-3">
<div class="col-12">
<div class="col-md-8">
<label for="url" class="form-label">Site URL'i</label>
<input type="url" class="form-control" id="url" name="url" placeholder="https://example.com">
<input type="text" class="form-control" id="url" name="url" placeholder="https://example.com">
<small class="text-muted">Site aktiflik kontrolü için gerekli</small>
</div>
<div class="col-md-4">
<label for="host_renewal_date" class="form-label">Host Yenileme Tarihi</label>
<input type="date" class="form-control" id="host_renewal_date" name="host_renewal_date">
<small class="text-muted">Hosting paketi yenileme tarihi</small>
</div>
</div>
</div>
</div>
@ -207,7 +237,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="clearLogsBtn" onclick="clearProjectLogs()" style="display: none;">
<button type="button" class="btn btn-danger" id="clearLogsBtn" onclick="ProjectManager.clearProjectLogs()" style="display: none;">
<i class="bi bi-trash"></i> Logları Temizle
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
@ -219,12 +249,12 @@
<!-- Meta Key Modal -->
<div class="modal fade" id="metaKeyModal" tabindex="-1" aria-labelledby="metaKeyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<div class="modal-content bg-dark text-light">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="metaKeyModalLabel">
<i class="bi bi-key me-2"></i>Site Doğrulama Meta Key
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Kapat"></button>
</div>
<div class="modal-body" id="metaKeyContent">
<div class="text-center">
@ -234,28 +264,113 @@
<p class="mt-2">Meta key bilgileri yükleniyor...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kapat</button>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Kapat</button>
</div>
</div>
</div>
</div>
<script>
// Tüm JavaScript fonksiyonları buradan project_list.html'den kopyalanacak
// Version: 2025-08-08-v4-{{ "now"|date:"YmdHis" }} - Fixed function definitions
// Ensure DOM is loaded before running scripts
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded, initializing project management functions...');
// CSRF Token almak için utility fonksiyon
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Toast bildirim fonksiyonu
function showToast(message, type = 'info', duration = 3000) {
// Toast container oluştur (eğer yoksa)
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 350px;
`;
document.body.appendChild(toastContainer);
}
// Toast elementi oluştur
const toast = document.createElement('div');
const bgClass = type === 'success' ? 'bg-success' :
type === 'error' ? 'bg-danger' :
type === 'warning' ? 'bg-warning' : 'bg-info';
toast.className = `alert ${bgClass} text-white mb-2`;
toast.style.cssText = `
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
toast.innerHTML = message;
// Toast'u container'a ekle
toastContainer.appendChild(toast);
// Animasyonla göster
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
}, 100);
// Belirtilen süre sonra kaldır
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, duration);
}
// ============= PROJE YÖNETİM FONKSİYONLARI =============
// Proje düzenleme fonksiyonu
window.editProject = function(id) {
function editProject(id) {
console.log('editProject called with ID:', id);
fetch(`/get-project-details/${id}/`)
.then(r => r.json())
.then(data => {
console.log('Project details response:', data);
if (data.success) {
document.getElementById('projectId').value = id;
document.getElementById('name').value = data.project.name;
document.getElementById('folder_name').value = data.project.folder_name;
document.getElementById('url').value = data.project.url || '';
// URL değerini ayarla - mevcut URL'yi olduğu gibi göster
const urlValue = data.project.url || '';
document.getElementById('url').value = urlValue;
// Host yenileme tarihini ayarla
const renewalDate = data.project.host_renewal_date || '';
document.getElementById('host_renewal_date').value = renewalDate;
document.getElementById('ssh_credential').value = data.project.ssh_credential_id || '';
document.getElementById('customer').value = data.project.customer_id || '';
document.getElementById('addProjectModalLabel').innerText = 'Proje Düzenle';
const modal = new bootstrap.Modal(document.getElementById('addProjectModal'));
@ -265,12 +380,47 @@ window.editProject = function(id) {
}
})
.catch(error => {
console.error('editProject error:', error);
showToast('❌ Proje bilgisi alınırken hata oluştu!', 'error');
});
}
function editProject(id) {
console.log('editProject called with ID:', id);
fetch(`/get-project-details/${id}/`)
.then(r => r.json())
.then data => {
console.log('Project details response:', data);
if (data.success) {
document.getElementById('projectId').value = id;
document.getElementById('name').value = data.project.name;
document.getElementById('folder_name').value = data.project.folder_name;
// URL değerini ayarla - mevcut URL'yi olduğu gibi göster
const urlValue = data.project.url || '';
document.getElementById('url').value = urlValue;
// Host yenileme tarihini ayarla
const renewalDate = data.project.host_renewal_date || '';
document.getElementById('host_renewal_date').value = renewalDate;
document.getElementById('ssh_credential').value = data.project.ssh_credential_id || '';
document.getElementById('customer').value = data.project.customer_id || '';
document.getElementById('addProjectModalLabel').innerText = 'Proje Düzenle';
const modal = new bootstrap.Modal(document.getElementById('addProjectModal'));
modal.show();
} else {
showToast('❌ Proje bilgisi alınamadı!', 'error');
}
})
.catch(error => {
console.error('editProject error:', error);
showToast('❌ Proje bilgisi alınırken hata oluştu!', 'error');
});
}
// Proje Sil
window.deleteProject = function(id) {
function deleteProject(id) {
if (confirm('Projeyi silmek istediğinize emin misiniz?')) {
fetch(`/delete_project/${id}/`, {
method: 'POST',
@ -289,210 +439,304 @@ window.deleteProject = function(id) {
}
// Proje Yedekle
window.backupProject = function(id) {
if (confirm('Projeyi yedeklemek istiyor musunuz?')) {
showToast('🔄 Yedekleme başlatılıyor...', 'info');
function backupProject(id) {
showToast('🔄 Proje yedekleniyor...', 'info');
fetch(`/backup-project/${id}/`, {
method: 'POST',
headers: { 'X-CSRFToken': getCookie('csrftoken') }
headers: {
'X-CSRFToken': getCookie('csrftoken'),
}
})
.then(r => r.json())
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('✅ Yedekleme başarılı', 'success');
setTimeout(() => {
window.showLogsByProject(id);
}, 800);
showToast('✅ Proje başarıyla yedeklendi!', 'success');
} else {
showToast(`${data.message}`, 'error');
}
})
.catch(error => {
showToast('❌ Yedekleme hatası!', 'error');
console.error('Backup error:', error);
showToast('❌ Yedekleme sırasında hata oluştu!', 'error');
});
}
}
// Log görüntüleme
window.showLogsByProject = function(projectId) {
window.currentProjectId = projectId;
function showLogsByProject(id) {
// Modal'ı
const logsModal = new bootstrap.Modal(document.getElementById('logsModal'));
logsModal.show();
fetch(`/project/${projectId}/backup-logs/`)
.then(r => r.json())
// Loading durumunu göster
document.getElementById('logsContent').innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Yükleniyor...</span>
</div>
<p class="mt-2">Loglar yükleniyor...</p>
</div>
`;
// Clear logs butonunu gizle
document.getElementById('clearLogsBtn').style.display = 'none';
fetch(`/project/${id}/backup-logs/`)
.then(response => response.json())
.then(data => {
let html = '';
let hasLogs = false;
if (data.success) {
console.log('Logs:', data.logs);
if (data.success && data.logs.length > 0) {
hasLogs = true;
html = '<div class="log-list">';
data.logs.forEach(function(log) {
let statusBadge = log.status ?
'<span class="badge bg-success me-2"><i class="bi bi-check-circle"></i> Başarılı</span>' :
'<span class="badge bg-danger me-2"><i class="bi bi-x-circle"></i> Hata</span>';
if (data.logs.length > 0) {
let logsHtml = '<div class="logs-container">';
let typeIcon = '';
if (log.log_type === 'backup') {
typeIcon = '<i class="bi bi-cloud-arrow-up text-warning me-2"></i>';
} else if (log.log_type === 'command') {
typeIcon = '<i class="bi bi-terminal text-info me-2"></i>';
}
data.logs.forEach(log => {
const statusIcon = log.status === 'success' ?
'<i class="bi bi-check-circle-fill text-success me-2"></i>' :
'<i class="bi bi-x-circle-fill text-danger me-2"></i>';
html += `
<div class="log-entry mb-3 p-3" style="background: rgba(255,255,255,0.05); border-radius: 8px; border-left: 3px solid ${log.status ? '#28a745' : '#dc3545'};">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>${statusBadge}${typeIcon}<strong>${log.command}</strong></div>
const logTypeIcon = log.log_type === 'backup' ?
'<i class="bi bi-cloud-arrow-up me-1"></i>' :
'<i class="bi bi-terminal me-1"></i>';
logsHtml += `
<div class="card mb-3 ${log.status === 'success' ? 'border-success' : 'border-danger'}">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<div>
${statusIcon}
${logTypeIcon}
<small class="text-muted">${log.created_at}</small>
</div>
<div class="log-output" style="font-family: monospace; font-size: 0.9em; color: #bdbdbd;">
${log.output.replace(/\n/g, '<br>')}
<span class="badge ${log.status === 'success' ? 'bg-success' : 'bg-danger'}">${log.status}</span>
</div>
<div class="card-body py-2">
<p class="mb-2"><strong>Komut:</strong></p>
<code class="d-block bg-dark text-light p-2 rounded mb-2">${log.command}</code>
<p class="mb-2"><strong>Çıktı:</strong></p>
<pre class="bg-light p-2 rounded text-dark" style="max-height: 200px; overflow-y: auto; font-size: 0.85rem;">${log.output}</pre>
</div>
</div>
`;
});
html += '</div>';
logsHtml += '</div>';
document.getElementById('logsContent').innerHTML = logsHtml;
// Clear logs butonunu göster
document.getElementById('clearLogsBtn').style.display = 'inline-block';
} else {
html = `
<div class="text-center py-4">
<i class="bi bi-journal-text" style="font-size: 2rem; color: #6c757d;"></i>
<p class="text-muted mt-2">Bu projeye ait log bulunamadı</p>
document.getElementById('logsContent').innerHTML = `
<div class="text-center text-muted py-4">
<i class="bi bi-journal-text" style="font-size: 3rem;"></i>
<h5 class="mt-3">Bu proje için henüz log kaydı bulunmuyor</h5>
<p class="text-muted">Yedekleme veya site kontrol işlemleri yapıldığında loglar burada görünecektir.</p>
</div>
`;
}
document.getElementById('logsContent').innerHTML = html;
const clearBtn = document.getElementById('clearLogsBtn');
clearBtn.style.display = hasLogs ? 'inline-block' : 'none';
const logsModal = new bootstrap.Modal(document.getElementById('logsModal'));
logsModal.show();
} else {
document.getElementById('logsContent').innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
${data.message}
</div>
`;
}
})
.catch(error => {
console.error('Logs error:', error);
document.getElementById('logsContent').innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Loglar yüklenirken hata oluştu!
</div>
`;
});
}
// Site durumu kontrol
window.checkSiteStatus = function(projectId) {
showToast('🔄 Site kontrol ediliyor...', 'info');
function checkSiteStatus(id) {
showToast('🔄 Site durumu kontrol ediliyor...', 'info');
fetch(`/project/${projectId}/check-site/`, {
fetch(`/project/${id}/check-site/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const statusText = data.status ? '✅ Site Aktif' : '❌ Site Pasif';
showToast(statusText, data.status ? 'success' : 'error');
setTimeout(() => location.reload(), 1500);
showToast('✅ Site kontrol tamamlandı!', 'success');
setTimeout(() => window.location.reload(), 1500);
} else {
showToast(`${data.message}`, 'error');
}
})
.catch(error => {
showToast('❌ Kontrol hatası!', 'error');
console.error('Site check error:', error);
showToast('❌ Site kontrol sırasında hata oluştu!', 'error');
});
}
// Meta key göster
window.showMetaKey = function(projectId) {
fetch(`/project/${projectId}/meta-key/`)
.then(response => response.json())
.then(data => {
if (data.success) {
const content = `
<div class="alert alert-info">
<h6><i class="bi bi-info-circle"></i> Kullanım Talimatları:</h6>
<p>Bu meta tag'ı sitenizin <code>&lt;head&gt;</code> bölümüne ekleyin:</p>
</div>
// Meta Key Göster
function showMetaKey(id) {
// Modal'ı
const metaKeyModal = new bootstrap.Modal(document.getElementById('metaKeyModal'));
metaKeyModal.show();
<div class="mb-3">
<label class="form-label"><strong>Meta Key:</strong></label>
<div class="input-group">
<input type="text" class="form-control" value="${data.meta_key}" readonly>
<button class="btn btn-outline-secondary" onclick="copyToClipboard('${data.meta_key}')">
<i class="bi bi-clipboard"></i>
</button>
// Loading durumunu göster
document.getElementById('metaKeyContent').innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Yükleniyor...</span>
</div>
</div>
<div class="mb-3">
<label class="form-label"><strong>HTML Meta Tag:</strong></label>
<div class="input-group">
<textarea class="form-control" rows="2" readonly>${data.meta_tag}</textarea>
<button class="btn btn-outline-secondary" onclick="copyToClipboard('${data.meta_tag}')">
<i class="bi bi-clipboard"></i>
</button>
</div>
</div>
<div class="alert alert-warning">
<small><i class="bi bi-exclamation-triangle"></i> Meta tag'ı ekledikten sonra "Site Kontrol" butonuyla doğrulama yapabilirsiniz.</small>
<p class="mt-2">Meta key bilgileri yükleniyor...</p>
</div>
`;
document.getElementById('metaKeyContent').innerHTML = content;
const modal = new bootstrap.Modal(document.getElementById('metaKeyModal'));
modal.show();
fetch(`/project/${id}/meta-key/`)
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Meta key:', data);
let metaKeyHtml = `
<div class="mb-4">
<h6 class="text-warning mb-3">
<i class="bi bi-info-circle me-2"></i>Site Doğrulama Talimatları
</h6>
<div class="alert alert-info">
<p class="mb-2">Sitenizin doğrulanması için aşağıdaki meta tag'i HTML'inizin &lt;head&gt; bölümüne ekleyin:</p>
<ol class="mb-0">
<li>Site yönetici paneline giriş yapın</li>
<li>Tema düzenleyicisini açın (header.php veya index.html)</li>
<li>Aşağıdaki meta tag'i &lt;head&gt; bölümüne ekleyin</li>
<li>Değişiklikleri kaydedin</li>
<li>"Site Doğrula" butonuna tıklayın</li>
</ol>
</div>
</div>
<div class="mb-4">
<label class="form-label text-warning fw-bold">Meta Verification Tag:</label>
<div class="input-group">
<input type="text" class="form-control bg-dark text-light border-secondary"
value='${data.meta_key}' readonly id="metaKeyInput">
<button class="btn btn-outline-info" type="button" onclick="ProjectManager.copyToClipboard('${data.meta_key}')">>
<i class="bi bi-clipboard"></i> Kopyala
</button>
</div>
<small class="text-muted">Bu meta tag'i sitenizin &lt;head&gt; bölümüne ekleyin</small>
</div>
<div class="mb-4">
<h6 class="text-warning mb-3">
<i class="bi bi-check-circle me-2"></i>Doğrulama İşlemi
</h6>
<div class="d-grid">
<button class="btn btn-success btn-lg" onclick="ProjectManager.verifySite(${id})">>
<i class="bi bi-shield-check me-2"></i>Site Doğrula
</button>
</div>
<small class="text-muted d-block text-center mt-2">
Meta tag'i ekledikten sonra bu butona tıklayın
</small>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Önemli:</strong> Meta tag'i eklemeden önce doğrulama yapmayın.
Tag eklendikten sonra 2-3 dakika bekleyip doğrulama yapın.
</div>
`;
document.getElementById('metaKeyContent').innerHTML = metaKeyHtml;
} else {
showToast(`${data.message}`, 'error');
document.getElementById('metaKeyContent').innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
${data.message}
</div>
`;
}
})
.catch(error => {
showToast('Meta key alınırken hata oluştu!', 'error');
console.error('Meta key error:', error);
document.getElementById('metaKeyContent').innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Meta key alınırken hata oluştu!
</div>
`;
});
}
// Clipboard'a kopyala
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast('📋 Panoya kopyalandı!', 'success');
showToast(' Panoya kopyalandı!', 'success');
}).catch(err => {
console.error('Copy error:', err);
showToast('❌ Kopyalama hatası!', 'error');
});
}
// Log temizleme
function clearProjectLogs() {
if (!window.currentProjectId) {
showToast('Proje ID bulunamadı!', 'error');
return;
}
// Site doğrula
function verifySite(projectId) {
showToast('🔄 Site doğrulama tamamlandı!', 'success');
if (!confirm('Bu projenin tüm loglarını silmek istediğinizden emin misiniz?')) {
return;
}
fetch(`/project/${window.currentProjectId}/clear-logs/`, {
fetch(`/project/${projectId}/check-site/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('logsModal'));
if (modal) modal.hide();
showToast(`${data.deleted_count} log kaydı silindi`, 'success');
window.currentProjectId = null;
showToast('✅ Site doğrulama tamamlandı!', 'success');
} else {
showToast(`${data.message || 'Log silme işlemi başarısız!'}`, 'error');
showToast(`${data.message}`, 'error');
}
})
.catch(error => {
showToast('❌ Log silme sırasında bir hata oluştu!', 'error');
console.error('Site doğrulama hatası:', error);
showToast('❌ Site doğrulama hatası!', 'error');
});
}
// Proje loglarını temizle
function clearProjectLogs() {
if (confirm('Tüm proje loglarını temizlemek istediğinize emin misiniz?')) {
showToast('🔄 Loglar temizleniyor...', 'info');
fetch('/logs/clear/', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('✅ Loglar temizlendi!', 'success');
setTimeout(() => window.location.reload(), 1500);
} else {
showToast(`${data.message}`, 'error');
}
})
.catch(error => {
console.error('Clear logs error:', error);
showToast('❌ Log temizleme hatası!', 'error');
});
}
}
// Proje Form Submit
document.getElementById('projectForm').addEventListener('submit', function(e) {
e.preventDefault();
const id = document.getElementById('projectId').value;
const url = id ? `/update-project/${id}/` : '/project/create/';
const formData = new FormData(this);
@ -515,40 +759,122 @@ document.getElementById('projectForm').addEventListener('submit', function(e) {
});
});
// Proje Arama
document.getElementById('projectSearch').addEventListener('keyup', function() {
const searchTerm = this.value.toLowerCase().trim();
// Durum Filtresi
document.getElementById('statusFilter').addEventListener('change', function() {
const filterValue = this.value;
const projectRows = document.querySelectorAll('tbody tr');
if (searchTerm.length < 2) {
projectRows.forEach(row => {
if (row.cells.length > 1) {
row.style.display = '';
const siteStatusCell = row.cells[3]; // Site Durumu sütunu
let shouldShow = false;
if (filterValue === '') {
// Tüm projeler
shouldShow = true;
} else if (filterValue === 'active') {
// Aktif projeler - success badge'i olanlar
shouldShow = siteStatusCell.querySelector('.badge.bg-success') !== null;
} else if (filterValue === 'inactive') {
// Pasif projeler - danger badge'i olanlar
shouldShow = siteStatusCell.querySelector('.badge.bg-danger') !== null;
} else if (filterValue === 'unknown') {
// Bilinmeyen durum - secondary badge'i olanlar
shouldShow = siteStatusCell.querySelector('.badge.bg-secondary') !== null;
} else if (filterValue === 'no-url') {
// URL'siz projeler - "URL tanımlanmamış" yazısı olanlar
shouldShow = siteStatusCell.textContent.includes('URL tanımlanmamış');
}
row.style.display = shouldShow ? '' : 'none';
}
});
return;
// Filtreleme sonucunda gösterilen proje sayısını güncelle
updateProjectCount();
});
// Proje sayısını güncelleme fonksiyonu
function updateProjectCount() {
const visibleRows = document.querySelectorAll('tbody tr[style=""], tbody tr:not([style])');
const totalRows = document.querySelectorAll('tbody tr').length;
// Eğer empty state row varsa onu sayma
const actualVisibleRows = Array.from(visibleRows).filter(row => row.cells.length > 1);
const actualTotalRows = Array.from(document.querySelectorAll('tbody tr')).filter(row => row.cells.length > 1);
// Başlığı güncelle
const headerText = document.querySelector('h3');
const originalText = headerText.textContent.split(' (')[0]; // Mevcut sayı bilgisini temizle
if (actualTotalRows.length > 0) {
if (actualVisibleRows.length !== actualTotalRows.length) {
headerText.innerHTML = `<i class="bi bi-folder-fill me-2"></i>Projeler (${actualVisibleRows.length}/${actualTotalRows.length})`;
} else {
headerText.innerHTML = `<i class="bi bi-folder-fill me-2"></i>Projeler (${actualTotalRows.length})`;
}
} else {
headerText.innerHTML = `<i class="bi bi-folder-fill me-2"></i>Projeler`;
}
}
// Proje Arama (güncellenmiş versiyon - filtreleme ile uyumlu)
document.getElementById('projectSearch').addEventListener('keyup', function() {
const searchTerm = this.value.toLowerCase().trim();
const statusFilter = document.getElementById('statusFilter').value;
const projectRows = document.querySelectorAll('tbody tr');
projectRows.forEach(row => {
if (row.cells.length > 1) {
let shouldShow = true;
// Önce arama terimine göre kontrol et
if (searchTerm.length >= 2) {
const projectInfo = row.cells[1].textContent.toLowerCase();
const folderInfo = row.cells[2].textContent.toLowerCase();
const siteInfo = row.cells[3].textContent.toLowerCase();
if (projectInfo.includes(searchTerm) || folderInfo.includes(searchTerm) || siteInfo.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
shouldShow = projectInfo.includes(searchTerm) ||
folderInfo.includes(searchTerm) ||
siteInfo.includes(searchTerm);
}
// Sonra durum filtresine göre kontrol et
if (shouldShow && statusFilter !== '') {
const siteStatusCell = row.cells[3];
if (statusFilter === 'active') {
shouldShow = siteStatusCell.querySelector('.badge.bg-success') !== null;
} else if (statusFilter === 'inactive') {
shouldShow = siteStatusCell.querySelector('.badge.bg-danger') !== null;
} else if (statusFilter === 'unknown') {
shouldShow = siteStatusCell.querySelector('.badge.bg-secondary') !== null;
} else if (statusFilter === 'no-url') {
shouldShow = siteStatusCell.textContent.includes('URL tanımlanmamış');
}
}
row.style.display = shouldShow ? '' : 'none';
}
});
// Proje sayısını güncelle
updateProjectCount();
});
// Proje Arama (eski kod - kaldırıldı)
// Modal reset
document.getElementById('addProjectModal').addEventListener('hidden.bs.modal', function () {
document.getElementById('projectForm').reset();
document.getElementById('projectId').value = '';
document.getElementById('addProjectModalLabel').textContent = 'Yeni Proje Ekle';
});
// Sayfa yüklendiğinde proje sayısını göster
updateProjectCount();
}); // DOMContentLoaded end
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-q3)mbwf)1sn6y+9x)&y_d35e)$mm6$&&^2n8i9dwv)kjz%7)6t
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['yonetim.alcom.dev']
ALLOWED_HOSTS = ['*']
CSRF_TRUSTED_ORIGINS = ['https://yonetim.alcom.dev']
@ -129,3 +129,8 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Oturum yönetimi ayarları
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

View File

@ -16,8 +16,11 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
path('', include('ssh_manager.urls')),
]