From 605e49e37293c681ff7264596c8ebc8f33b177d9 Mon Sep 17 00:00:00 2001 From: AyrisAI Date: Fri, 3 Jul 2026 11:37:20 +0300 Subject: [PATCH] first commit --- src/app/api/analytics/stats/route.ts | 40 +- src/app/api/backups/route.ts | 12 +- src/app/api/backups/test/route.ts | 10 +- src/app/api/db/backup/route.ts | 11 +- src/app/dashboard/analytics/page.tsx | 713 +++++++------ src/app/dashboard/connections/page.tsx | 270 +++-- src/app/dashboard/databases/page.tsx | 1253 +++++++++++++++-------- src/app/dashboard/docker/ClientPage.tsx | 436 ++++---- src/app/dashboard/insight/page.tsx | 513 +++++----- src/app/dashboard/page.tsx | 259 +++-- src/app/dashboard/services/page.tsx | 238 +++-- src/app/dashboard/uptime/page.tsx | 483 ++++++--- src/lib/appDb.ts | 24 +- src/lib/cronWorker.ts | 12 +- 14 files changed, 2712 insertions(+), 1562 deletions(-) diff --git a/src/app/api/analytics/stats/route.ts b/src/app/api/analytics/stats/route.ts index 5f67f83..6bba86d 100644 --- a/src/app/api/analytics/stats/route.ts +++ b/src/app/api/analytics/stats/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { requireAuth } from '@/lib/auth' -import { getAnalyticsStats, getAllDomains, addAnalyticsSite, renameAnalyticsSite, deleteAnalyticsSite } from '@/lib/appDb' +import { getAnalyticsStats, getAllDomains, addAnalyticsSite, renameAnalyticsSite, deleteAnalyticsSite, updateAnalyticsSiteName } from '@/lib/appDb' export async function GET(req: NextRequest) { const err = await requireAuth(req) @@ -9,24 +9,29 @@ export async function GET(req: NextRequest) { const domain = req.nextUrl.searchParams.get('domain') const days = parseInt(req.nextUrl.searchParams.get('days') ?? '30') - if (!domain) { - // Domain listesi döndür - const domains = await getAllDomains() - return NextResponse.json({ domains }) - } + try { + if (!domain) { + // Domain listesi döndür + const domains = await getAllDomains() + return NextResponse.json({ domains }) + } - const stats = await getAnalyticsStats(domain, days) - return NextResponse.json(stats) + const stats = await getAnalyticsStats(domain, days) + return NextResponse.json(stats) + } catch (error: any) { + console.error('GET stats error:', error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } } export async function POST(req: NextRequest) { const err = await requireAuth(req) if (err) return err - const { domain } = await req.json() + const { domain, name } = await req.json() if (!domain) return NextResponse.json({ error: 'Domain required' }, { status: 400 }) - await addAnalyticsSite(domain) + await addAnalyticsSite(domain, name) return NextResponse.json({ ok: true }) } @@ -34,10 +39,19 @@ export async function PUT(req: NextRequest) { const err = await requireAuth(req) if (err) return err - const { oldDomain, newDomain } = await req.json() - if (!oldDomain || !newDomain) return NextResponse.json({ error: 'Missing params' }, { status: 400 }) + const { oldDomain, newDomain, name } = await req.json() + + if (oldDomain && newDomain) { + await renameAnalyticsSite(oldDomain, newDomain) + if (name !== undefined) { + await updateAnalyticsSiteName(newDomain, name) + } + } else if (oldDomain && name !== undefined) { + await updateAnalyticsSiteName(oldDomain, name) + } else { + return NextResponse.json({ error: 'Missing params' }, { status: 400 }) + } - await renameAnalyticsSite(oldDomain, newDomain) return NextResponse.json({ ok: true }) } diff --git a/src/app/api/backups/route.ts b/src/app/api/backups/route.ts index 5d9481e..b51772f 100644 --- a/src/app/api/backups/route.ts +++ b/src/app/api/backups/route.ts @@ -66,15 +66,21 @@ export async function POST(req: NextRequest) { } } -// Delete backup config +// Delete backup config or backup log export async function DELETE(req: NextRequest) { const err = await requireAuth(req) if (err) return err try { const body = await req.json() - const { db_id } = body - if (!db_id) return NextResponse.json({ error: 'Missing dbId' }, { status: 400 }) + const { db_id, logId } = body + + if (logId) { + await pool.query(`DELETE FROM backup_logs WHERE id = $1`, [logId]) + return NextResponse.json({ ok: true }) + } + + if (!db_id) return NextResponse.json({ error: 'Missing db_id or logId' }, { status: 400 }) await pool.query(`DELETE FROM backup_configs WHERE db_id = $1`, [db_id]) await reloadBackupCrons() diff --git a/src/app/api/backups/test/route.ts b/src/app/api/backups/test/route.ts index b4a645d..a456d53 100644 --- a/src/app/api/backups/test/route.ts +++ b/src/app/api/backups/test/route.ts @@ -33,6 +33,7 @@ export async function POST(req: NextRequest) { const filename = type === 'gdrive' ? backupFilename() : backupFilename(db.name) + const backupStart = Date.now() try { const buffer = await createDbDumpBuffer(db) @@ -46,9 +47,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ ok: false, error: 'Desteklenmeyen cloud türü.' }) } + const durationSec = Math.round((Date.now() - backupStart) / 1000) + await pool.query( - `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, - [db_id, 'success', `Manuel yedek — ${type}`, buffer.length] + `INSERT INTO backup_logs (db_id, status, message, file_size, duration_sec) VALUES ($1, $2, $3, $4, $5)`, + [db_id, 'success', `Manuel yedek — ${type}`, buffer.length, durationSec] ) const sizeStr = buffer.length > 1024 * 1024 @@ -58,7 +61,8 @@ export async function POST(req: NextRequest) { const pathStr = type === 'gdrive' ? `${db.name}/${filename}` : filename return NextResponse.json({ ok: true, detail: `✓ Yüklendi — ${pathStr} (${sizeStr})` }) } catch (e: any) { - await pool.query(`INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, [db_id, 'error', e.message, 0]).catch(() => {}) + const durationSec = Math.round((Date.now() - backupStart) / 1000) + await pool.query(`INSERT INTO backup_logs (db_id, status, message, file_size, duration_sec) VALUES ($1, $2, $3, $4, $5)`, [db_id, 'error', e.message, 0, durationSec]).catch(() => {}) return NextResponse.json({ ok: false, error: e.message }) } } diff --git a/src/app/api/db/backup/route.ts b/src/app/api/db/backup/route.ts index 9776a50..fd13c0e 100644 --- a/src/app/api/db/backup/route.ts +++ b/src/app/api/db/backup/route.ts @@ -17,14 +17,16 @@ export async function GET(req: NextRequest) { const db = config.databases.find(d => d.id === dbId) if (!db) return NextResponse.json({ error: 'Database not found' }, { status: 404 }) + const backupStart = Date.now() try { const buffer = await createDbDumpBuffer(db) const dateStr = new Date().toISOString().replace(/[:.]/g, '-') const filename = `${db.name}_${dateStr}.sql` + const durationSec = Math.round((Date.now() - backupStart) / 1000) await pool.query( - `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, - [dbId, 'success', 'Manuel indirme', buffer.length] + `INSERT INTO backup_logs (db_id, status, message, file_size, duration_sec) VALUES ($1, $2, $3, $4, $5)`, + [dbId, 'success', 'Manuel indirme', buffer.length, durationSec] ) return new NextResponse(new Uint8Array(buffer), { @@ -34,9 +36,10 @@ export async function GET(req: NextRequest) { }, }) } catch (e: any) { + const durationSec = Math.round((Date.now() - backupStart) / 1000) await pool.query( - `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, - [dbId, 'error', e.message, 0] + `INSERT INTO backup_logs (db_id, status, message, file_size, duration_sec) VALUES ($1, $2, $3, $4, $5)`, + [dbId, 'error', e.message, 0, durationSec] ) return NextResponse.json({ error: e.message }, { status: 500 }) } diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx index fdb1c24..e756728 100644 --- a/src/app/dashboard/analytics/page.tsx +++ b/src/app/dashboard/analytics/page.tsx @@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { AnimatedTabs } from '@/components/ui/animated-tabs' -import { Edit2, Trash2, Plus, Code, Globe, CalendarDays } from 'lucide-react' +import { Edit2, Trash2, Plus, Code, Globe, CalendarDays, LineChart, CheckCircle2, Copy, Loader2, ArrowUpRight } from 'lucide-react' type Stats = { total: number @@ -25,25 +25,27 @@ const DAYS_OPTIONS = [ { id: '90', label: '90G' }, ] -function Bar({ label, value, max, color = 'var(--color-accent)' }: { label: string; value: number; max: number; color?: string }) { +function Bar({ label, value, max, color = '#00e5ff' }: { label: string; value: number; max: number; color?: string }) { const pct = max > 0 ? (value / max) * 100 : 0 return ( -
-
- {label} - {value.toLocaleString()} +
+
+ {label} + {value.toLocaleString()}
-
-
+
+
) } function MiniChart({ data, days }: { data: { day: string; views: number; visitors: number }[]; days: number }) { - if (!data.length) return
Henüz veri yok
+ if (!data.length) return
No data collected yet
- // Tüm günleri doldur const filled: { day: string; views: number; visitors: number }[] = [] const now = new Date() for (let i = days - 1; i >= 0; i--) { @@ -57,17 +59,23 @@ function MiniChart({ data, days }: { data: { day: string; views: number; visitor const maxViews = Math.max(...filled.map(d => d.views), 1) return ( -
+
{filled.map((d, i) => (
+ {d.day}: {d.views} views +
+
0 ? 4 : 0)}%`, - minHeight: d.views > 0 ? 2 : 0 + height: `${Math.max((d.views / maxViews) * 100, d.views > 0 ? 5 : 0)}%`, + minHeight: d.views > 0 ? 2 : 0, + boxShadow: d.views > 0 ? '0 0 8px rgba(0,229,255,0.2)' : 'none' }} />
@@ -77,30 +85,42 @@ function MiniChart({ data, days }: { data: { day: string; views: number; visitor } export default function AnalyticsPage() { - const [domains, setDomains] = useState([]) + const [domains, setDomains] = useState<{ domain: string; name: string | null }[]>([]) const [selectedDomain, setSelectedDomain] = useState(null) const [days, setDays] = useState('30') const [stats, setStats] = useState(null) const [loading, setLoading] = useState(false) const [showSnippet, setShowSnippet] = useState(false) const [showAddSite, setShowAddSite] = useState(false) - const [newDomain, setNewDomain] = useState('') + const [editingSite, setEditingSite] = useState<{ domain: string; name: string | null } | null>(null) const [panelUrl, setPanelUrl] = useState('') + const [copied, setCopied] = useState(false) const loadDomains = useCallback(() => { fetch('/api/analytics/stats') - .then(r => r.json()) + .then(async r => { + if (!r.ok) { + const errData = await r.json().catch(() => ({})); + throw new Error(errData.error || `HTTP ${r.status}`); + } + return r.json() + }) .then(d => { const dms = d.domains ?? [] setDomains(dms) if (dms.length > 0) { - if (!selectedDomain || !dms.includes(selectedDomain)) { - setSelectedDomain(dms[0]) + // Check if we still have the selected domain + const match = dms.find((x: any) => x.domain === selectedDomain) + if (!selectedDomain || !match) { + setSelectedDomain(dms[0].domain) } } else { setSelectedDomain(null) } }) + .catch(err => { + console.error('Error loading domains:', err) + }) }, [selectedDomain]) useEffect(() => { @@ -111,36 +131,27 @@ export default function AnalyticsPage() { const loadStats = useCallback(async () => { if (!selectedDomain) return setLoading(true) - const r = await fetch(`/api/analytics/stats?domain=${encodeURIComponent(selectedDomain)}&days=${days}`) - setStats(await r.json()) + try { + const r = await fetch(`/api/analytics/stats?domain=${encodeURIComponent(selectedDomain)}&days=${days}`) + if (!r.ok) { + const errData = await r.json().catch(() => ({})); + throw new Error(errData.error || `HTTP ${r.status}`); + } + setStats(await r.json()) + } catch (err) { + console.error('Error loading stats:', err) + } setLoading(false) }, [selectedDomain, days]) useEffect(() => { loadStats() }, [loadStats]) - const handleRename = async () => { - if (!selectedDomain) return - const newName = window.prompt("Yeni alan adını girin (Örn: example.com):", selectedDomain) - if (!newName || newName.trim() === '' || newName === selectedDomain) return - - setLoading(true) - await fetch('/api/analytics/stats', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ oldDomain: selectedDomain, newDomain: newName.trim() }) - }) - setSelectedDomain(newName.trim()) - - setTimeout(loadDomains, 100) - } - - const handleDelete = async () => { - if (!selectedDomain) return - if (!window.confirm(`'${selectedDomain}' sitesini ve tüm analiz verilerini SİLMEK istediğinize emin misiniz? Bu işlem geri alınamaz!`)) return + const handleDelete = async (dName: string) => { + if (!window.confirm(`'${dName}' sitesini ve tüm analiz verilerini SİLMEK istediğinize emin misiniz? Bu işlem geri alınamaz!`)) return setLoading(true) - await fetch(`/api/analytics/stats?domain=${encodeURIComponent(selectedDomain)}`, { method: 'DELETE' }) - setSelectedDomain(null) + await fetch(`/api/analytics/stats?domain=${encodeURIComponent(dName)}`, { method: 'DELETE' }) + if (selectedDomain === dName) setSelectedDomain(null) setTimeout(loadDomains, 100) } @@ -148,212 +159,290 @@ export default function AnalyticsPage() { const snippet = ` ` + const selectedSite = domains.find(d => d.domain === selectedDomain) + return ( -
- {/* Header */} -
-
-

Analytics

-

Cookie-free, self-hosted website tracking

+
+ {/* Analyticus Top Header Bar */} +
+
+
+ +
+ Analyticus
-
- {/* Domain Actions */} - {selectedDomain && ( -
- -
- +
+ + + Community + +
+
+ + {/* Main Split Layout */} +
+ + {/* Left Sidebar: Domains selector list */} + + + {/* Right Panel: Domain Analytics Detail */} +
+ {!selectedDomain ? ( +
+ +
+ Sol menüden bir alan adı seçin veya yeni bir alan adı ekleyerek analiz metriklerini görüntüleyin. +
+
+ ) : ( +
+ + {/* Domain detail header */} +
+
+

+ {selectedSite?.name || selectedDomain} +

+ + https://{selectedDomain} + +
+ +
+ {/* Days tab filters */} +
+ setDays(id)} + tabs={DAYS_OPTIONS} + className="border-0 bg-transparent p-0" + /> +
+
+
+ + {stats && !loading ? ( +
+ {/* Metrics cards row */} +
+ {[ + { label: 'Toplam Görüntüleme', value: stats.total.toLocaleString(), color: 'text-accent' }, + { label: 'Tekil Ziyaretçi', value: stats.unique.toLocaleString(), color: 'text-[#00e676]' }, + ].map(s => ( + + +
{s.label}
+
{s.value}
+
+ son {days} gün +
+
+
+ ))} +
+ + {/* Daily Chart */} + + + Günlük Görüntülemeler + + + +
+ {days} gün önce + bugün +
+
+
+ + {/* 3-column analytics progress breakdown */} +
+ {/* Top pages */} + + + Sayfalar + + + {stats.topPages.length === 0 ? ( +
Veri yok
+ ) : ( + stats.topPages.map(p => ) + )} +
+
+ + {/* Traffic Referrers */} + + + Trafik Kaynağı + + + {stats.topReferrers.length === 0 ? ( +
Direct / bilinmiyor
+ ) : ( + stats.topReferrers.map(r => { + let label = r.referrer + try { label = new URL(r.referrer).hostname } catch { /* ignore */ } + return + }) + )} +
+
+ + {/* Countries list */} + + + Ülkeler + + + {stats.byCountry.length === 0 ? ( +
Cloudflare proxy gerekli
+ ) : ( + stats.byCountry.map(c => ) + )} +
+
+
+ + {/* Device / Browser / OS breakdown */} +
+ {[ + { title: 'Cihaz', data: stats.byDevice.map(d => ({ label: d.device, value: d.n })), color: '#7b61ff' }, + { title: 'Tarayıcı', data: stats.byBrowser.map(d => ({ label: d.browser, value: d.n })), color: '#7b61ff' }, + { title: 'İşletim Sistemi', data: stats.byOs.map(d => ({ label: d.os, value: d.n })), color: '#7b61ff' }, + ].map(section => ( + + + {section.title} + + + {section.data.length === 0 ? ( +
Veri yok
+ ) : ( + section.data.map(d => ) + )} +
+
+ ))} +
+
+ ) : null} + + {loading && ( +
+ + Veriler yükleniyor... +
+ )}
)} - - - - - - {/* Days filter - using AnimatedTabs */} -
- setDays(id)} - tabs={DAYS_OPTIONS} - /> -
-
+
- {/* Domain tabs */} - {domains.length > 0 ? ( -
- {domains.map(d => ( - - ))} -
- ) : ( - - - -

Henüz veri yok

-

Sitelerine tracking snippet ekle, veriler burada görünecek.

- -
-
- )} - - {stats && !loading && ( -
- {/* Big numbers */} -
- {[ - { label: 'Toplam Görüntüleme', value: stats.total.toLocaleString(), color: 'text-accent' }, - { label: 'Tekil Ziyaretçi', value: stats.unique.toLocaleString(), color: 'text-success' }, - ].map(s => ( - - -
{s.label}
-
{s.value}
-
- son {days} gün -
-
-
- ))} -
- - {/* Chart */} - - - Günlük Görüntülemeler - - - -
- {days} gün önce - bugün -
-
-
- - {/* 3-col breakdown */} -
- {/* Top pages */} - - - Sayfalar - - - {stats.topPages.length === 0 - ?
Veri yok
- : stats.topPages.map(p => ) - } -
-
- - {/* Referrers */} - - - Trafik Kaynağı - - - {stats.topReferrers.length === 0 - ?
Direct / bilinmiyor
- : stats.topReferrers.map(r => { - let label = r.referrer - try { label = new URL(r.referrer).hostname } catch { /* ignore */ } - return - }) - } -
-
- - {/* Countries */} - - - Ülkeler - - - {stats.byCountry.length === 0 - ?
Cloudflare proxy gerekli
- : stats.byCountry.map(c => ) - } -
-
-
- - {/* Device / Browser / OS */} -
- {[ - { title: 'Cihaz', data: stats.byDevice.map(d => ({ label: d.device, value: d.n })) }, - { title: 'Tarayıcı', data: stats.byBrowser.map(d => ({ label: d.browser, value: d.n })) }, - { title: 'İşletim Sistemi', data: stats.byOs.map(d => ({ label: d.os, value: d.n })) }, - ].map(section => ( - - - {section.title} - - - {section.data.length === 0 - ?
Veri yok
- : section.data.map(d => ) - } -
-
- ))} -
-
- )} - - {loading && ( -
Yükleniyor...
- )} - {/* Snippet modal */} {showSnippet && ( -
setShowSnippet(false)}> - e.stopPropagation()}> - - Tracking Snippet - - Sitendeki {''} tagının içine ekle: +
setShowSnippet(false)}> + e.stopPropagation()}> + + Tracking Snippet + + Sitendeki {''} tagının içine ekle: - -
-                {snippet}
-              
- -
-
data-domain — hangi domain olduğunu belirtir, değiştirme
-
defer — sayfayı yavaşlatmaz, arka planda yüklenir
+ +
+
+                  {snippet}
+                
-
- - +
@@ -361,63 +450,103 @@ export default function AnalyticsPage() {
)} - {/* Add Site modal */} - {showAddSite && ( -
setShowAddSite(false)}> - e.stopPropagation()}> - - Yeni Site Ekle - Takip etmek istediğiniz alan adını girin (örn: example.com) - - - setNewDomain(e.target.value)} - autoFocus - onKeyDown={e => { - if (e.key === 'Enter' && newDomain.trim()) { - fetch('/api/analytics/stats', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ domain: newDomain.trim() }) - }).then(() => { - setNewDomain(''); - setShowAddSite(false); - loadDomains(); - setSelectedDomain(newDomain.trim()); - }) - } - }} - /> - -
- - -
-
-
-
+ {/* Add / Edit Site modal */} + {(showAddSite || editingSite) && ( + { setShowAddSite(false); setEditingSite(null) }} + onSaved={() => { loadDomains(); setShowAddSite(false); setEditingSite(null) }} + /> )}
) } + +function AddSiteModal({ + site, + onClose, + onSaved +}: { + site?: { domain: string; name: string | null }; + onClose: () => void; + onSaved: () => void +}) { + const [domain, setDomain] = useState(site?.domain || '') + const [name, setName] = useState(site?.name || '') + const [loading, setLoading] = useState(false) + + const submit = async () => { + setLoading(true) + if (site) { + // Editing + await fetch('/api/analytics/stats', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + oldDomain: site.domain, + newDomain: domain.trim(), + name: name.trim() || null + }) + }) + } else { + // Adding + await fetch('/api/analytics/stats', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + domain: domain.trim(), + name: name.trim() || null + }) + }) + } + setLoading(false) + onSaved() + } + + return ( +
+ e.stopPropagation()}> + + {site ? 'Site Düzenle' : 'Yeni Site Ekle'} + + {site ? 'Sitenin alan adını veya görünen ismini güncelleyin.' : 'Takip etmek istediğiniz web sitesinin detaylarını girin.'} + + + +
+ + setName(e.target.value)} + className="bg-black/20 border-[#1d2639] focus-visible:ring-accent/50 text-sm" + autoFocus={!!site} + /> +
+ +
+ + setDomain(e.target.value)} + className="bg-black/20 border-[#1d2639] focus-visible:ring-accent/50 font-mono text-sm" + autoFocus={!site} + /> +
+ +
+ + +
+
+
+
+ ) +} diff --git a/src/app/dashboard/connections/page.tsx b/src/app/dashboard/connections/page.tsx index 333ef0c..91d956f 100644 --- a/src/app/dashboard/connections/page.tsx +++ b/src/app/dashboard/connections/page.tsx @@ -1,13 +1,13 @@ 'use client' import { useState, useEffect, useCallback } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { Plus, Trash2, CheckCircle2, Link2, RefreshCw, ShieldAlert, Copy } from 'lucide-react' +import { Plus, Trash2, CheckCircle2, Link2, RefreshCw, ShieldAlert, Copy, Cloud, Edit2 } from 'lucide-react' type Conn = { id: string; name: string; type: 'gcs' | 'gdrive' | 'dropbox'; credentials: string; default_target: string | null } -const TYPE_LABELS: Record = { gcs: '☁️ Google Cloud Storage', gdrive: '📁 Google Drive', dropbox: '📦 Dropbox' } +const TYPE_LABELS: Record = { gcs: 'Google Cloud Storage', gdrive: 'Google Drive', dropbox: 'Dropbox' } function isGdriveConnected(credentials: string) { try { return !!JSON.parse(credentials).refresh_token } catch { return false } @@ -42,80 +42,140 @@ export default function ConnectionsPage() { }, [load]) const del = async (id: string) => { - if (!confirm('Bu bağlantıyı sil?')) return + if (!confirm('Bu bağlantıyı silmek istediğinize emin misiniz?')) return await fetch(`/api/cloud-connections?id=${id}`, { method: 'DELETE' }) load() } return ( -
-
-
-

Cloud Bağlantıları

-

Bir kere kur, tüm DB yedekleri için kullan

+
+ {/* Connectus Top Header Bar */} +
+
+
+ +
+ Connectus +
+ +
+ + + Community + +
+
+ + {/* Main Content Area */} +
+
+ + {/* Subtitle description */} +
+

+ Bir kere bulut entegrasyonu kurun, tüm veritabanı yedekleriniz için hedefler olarak kullanın +

+
+ + {/* Success/Error alert notice */} + {notice && ( +
+ {notice.ok ? : } + {notice.msg} + +
+ )} + + {/* Connections list */} + {conns.length === 0 ? ( + + + +

Henüz bağlantı bulunmuyor

+

+ Dropbox, Google Drive veya Google Cloud Storage entegrasyonlarınızı buradan yapılandırın. +

+ +
+
+ ) : ( +
+ {conns.map(conn => { + const gdriveOk = conn.type === 'gdrive' && isGdriveConnected(conn.credentials) + + // Set custom icon prefix + let iconPrefix = '☁️' + if (conn.type === 'gdrive') iconPrefix = '📁' + if (conn.type === 'dropbox') iconPrefix = '📦' + + return ( + + +
+
+ {conn.name} + + {iconPrefix} {TYPE_LABELS[conn.type] ?? conn.type} + + {conn.type === 'gdrive' && ( + + {gdriveOk ? '✓ Bağlı' : '⚠ Yetki Yok'} + + )} +
+
+ {conn.default_target ? `Varsayılan Hedef: ${conn.default_target}` : 'Varsayılan klasör/bucket hedeflenmedi'} +
+
+ +
+ {conn.type === 'gdrive' && ( + + )} + + + +
+
+
+ ) + })} +
+ )} +
-
- {notice && ( -
- {notice.ok ? : } - {notice.msg} - -
- )} - - {conns.length === 0 ? ( - - - -
Henüz bağlantı yok
-

GCS, Google Drive veya Dropbox bağlantısı ekle.

- -
-
- ) : ( -
- {conns.map(conn => { - const gdriveOk = conn.type === 'gdrive' && isGdriveConnected(conn.credentials) - return ( - - -
-
- {conn.name} - - {TYPE_LABELS[conn.type] ?? conn.type} - - {conn.type === 'gdrive' && ( - - {gdriveOk ? '✓ Bağlı' : '⚠ Yetki Yok'} - - )} -
-
- {conn.default_target ? `Hedef: ${conn.default_target}` : 'Varsayılan hedef yok'} -
-
-
- {conn.type === 'gdrive' && ( - - )} - - -
-
-
- ) - })} -
- )} - + {/* Add / Edit Connection Modal overlay */} {(showAdd || editConn) && ( - e.stopPropagation()}> - - {conn ? 'Bağlantı Düzenle' : 'Yeni Cloud Bağlantısı'} +
+ e.stopPropagation()}> + + {conn ? 'Bağlantı Düzenle' : 'Yeni Cloud Bağlantısı'} + Yedek hedefleri olarak kullanabileceğiniz hesap yetkilendirmelerini girin. - + +
- setName(e.target.value)} placeholder="Production Drive, Backup GCS…" /> + setName(e.target.value)} placeholder="Örn: Production Google Drive" className="bg-black/20 border-[#1d2639]" />
{(['gcs', 'gdrive', 'dropbox'] as const).map(t => ( - ))}
- {/* GCS */} + {/* GCS Fields */} {type === 'gcs' && ( <>
-