From c1f04735d6ca0b6cb17c9527bf9a417db4898c65 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Fri, 5 Jun 2026 23:53:57 +0300 Subject: [PATCH] feat: update vps-panel backup and notification features --- config.json.bak | 115 -------- data/logs.db | Bin 28672 -> 0 bytes data/panel.db | 0 package-lock.json | 7 - package.json | 1 - src/app/api/auth/login/route.ts | 43 ++- src/app/api/backups/route.ts | 18 +- src/app/api/backups/test/route.ts | 62 +++++ src/app/api/db/backup/route.ts | 26 +- src/app/api/docker/route.ts | 118 +++++++-- src/app/api/notifications/route.ts | 23 ++ src/app/dashboard/databases/page.tsx | 338 +++++++++++++++++++----- src/app/dashboard/docker/ClientPage.tsx | 177 +++++++++++-- src/app/dashboard/services/page.tsx | 62 +++-- src/app/dashboard/uptime/page.tsx | 83 +++++- src/lib/appDb.ts | 29 ++ src/lib/backup.ts | 181 ++++++++++--- src/lib/config.ts | 33 --- src/lib/cronWorker.ts | 9 +- src/lib/db.ts | 14 +- src/lib/notify.ts | 52 ++++ src/lib/ping.ts | 10 +- 22 files changed, 1029 insertions(+), 372 deletions(-) delete mode 100644 config.json.bak delete mode 100644 data/logs.db delete mode 100644 data/panel.db create mode 100644 src/app/api/backups/test/route.ts create mode 100644 src/app/api/notifications/route.ts create mode 100644 src/lib/notify.ts diff --git a/config.json.bak b/config.json.bak deleted file mode 100644 index 0ff72b4..0000000 --- a/config.json.bak +++ /dev/null @@ -1,115 +0,0 @@ -{ - "sites": [ - { - "name": "Ayirs Tech", - "url": "https://ayris.tech", - "interval_min": 1, - "id": "1779887873514-4l00t" - }, - { - "name": "Onx Control", - "url": "http://onxcontrol.com", - "interval_min": 1, - "id": "1779887898264-992ox" - } - ], - "databases": [ - { - "name": "Leyüze Butik Chat", - "host": "65.109.236.58", - "port": 3647, - "database": "postgres", - "username": "postgres", - "password": "4T1FnNSDlhhWT1W5bOp3ti6LKw4AFzCarrfXLo2lUuXP8iEvUFWQ3SZpGgnK4C5X", - "ssl": false, - "color": "#00e5ff", - "id": "1779888243791-oyi2i" - }, - { - "name": "AyçaNurTuran", - "host": "65.109.236.58", - "port": 6482, - "database": "postgres", - "username": "postgres", - "password": "P9cIY8Ji1iSXOCRs9q6WbOo5xeXCdzyQjYoQ511Zmq1RY8WHLU9YKBGyjDpJ02sa", - "ssl": false, - "color": "#00e5ff", - "id": "1779888647024-853cg" - }, - { - "name": "AyrisApart", - "host": "65.109.236.58", - "port": 9435, - "database": "postgres", - "username": "postgres", - "password": "byCfEjXq0Tflw4K0mZ74qczYoajwcUFJFl5WWNTHoxOirgw1NDyoJMEVtyCJBBbk", - "ssl": false, - "color": "#00e5ff", - "id": "1779889157629-xpe3j" - }, - { - "name": "X-Tracker", - "host": "65.109.236.58", - "port": 5433, - "database": "postgres", - "username": "postgres", - "password": "6UuYjVZ13ZKfmgKRIsXiCqNjLriAkvugGG9awYqM4BXo78Sg39JypbyNgV72K0zY", - "ssl": false, - "color": "#00e5ff", - "id": "1779889184466-xsq40" - }, - { - "name": "Luna QR Menu", - "host": "65.109.236.58", - "port": 4378, - "database": "postgres", - "username": "postgres", - "password": "Oesb5U5YQQAgIByAZZVn3WeL9hqFME72ReI0nUD5EGC546fUINxuheEaLbvsL3jM", - "ssl": false, - "color": "#00e5ff", - "id": "1779889217515-g9t8s" - }, - { - "name": "Mugla Dijital Medya", - "host": "65.109.236.58", - "port": 8392, - "database": "postgres", - "username": "postgres", - "password": "xGhPj4IuE5VocaxUoYAj1dSr2xf6M3hh3c2C6YbnB7ZOeVJLRvmL0mzCbhvf14dh", - "ssl": false, - "color": "#00e5ff", - "id": "1779889251060-zk9yi" - }, - { - "name": "App-Admin", - "host": "65.109.236.58", - "port": 7397, - "database": "postgres", - "username": "postgres", - "password": "D7Vk2Pu55kixWc0vTqRXsn2SEHtlbcdZBDavGkirOQM45rsYf2QFhKLBzhggvE6c", - "ssl": false, - "color": "#00e5ff", - "id": "1779889294957-vbql4" - }, - { - "name": "VPS Panel", - "host": "65.109.236.58", - "port": 4389, - "database": "postgres", - "username": "postgres", - "password": "8VwBw9pepLzDpJITPies6yUVp9SHHsjgZlEDSyM4L3m7JmnP32ywO28FHw05PTUL", - "ssl": false, - "color": "#00e5ff", - "id": "1779889337654-9bfak" - } - ], - "services": [ - { - "id": "svc-1", - "name": "Coolify", - "url": "http://localhost:8000", - "icon": "⚙️", - "description": "Deploy yönetimi" - } - ] -} \ No newline at end of file diff --git a/data/logs.db b/data/logs.db deleted file mode 100644 index 313ab05b4ca441e47fc0ca081a5f390f66efed7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI(!B5jr90%}r>sGIDvB)&q-M1j8~I5M$zmN*2e!F%rc^~jNd;J3b7@%TvQrrv5~LT^M+oLx0Sl8?Cbn|R!Zh1 zF_l@6)1EbjP0t6c z)nxbW4A$eS8A92y7Q|-H?r%soo`6a;Br%mA#OP%|+lkO_rQyF1fZ< z(3((tuxC#veKzNVzZl_I_#nI#ioy+Hm>i-(00Izz00bZa0SG_<0uX=z1pa4%Fn^AT zU-WnwE;chWHyck(PEAk85)(UeL2lG4-jB@Z2gAqT!v4;**woZqY~tbUY&}lW-qE!1 zV%XQ2=BnoTYNkzwx!!57Y1-IP*xNJBcil`o40}4$++#D$w1Y6)GtI}FX&Qb0XNBJk z`9XsK1Rwwb2tWV=5P$##AOHafK%id)hFEWibv|?82g!Z$LV^)ig-^l{At6+RSHk;# zxmnx?1Rwwb2tWV=5P$##AOHafK;Ul#M!7i0cKL#UemlUsf^F~S=@$cjSFr7C0TP__ zogN%J;(7!Ry{@3E&#?o~B{JFpd$jJL`Tn2$(?1#nAOHafKmY;|fB*y_009U<00Mn1 zK;Qr4`M-}R7`Fxi2tWV=5P$##AOHafKmY;|AOd*)$1;Eb1Rwwb2tWV=5P$##AOHaf H^uNF#L`17A diff --git a/data/panel.db b/data/panel.db deleted file mode 100644 index e69de29..0000000 diff --git a/package-lock.json b/package-lock.json index bc7b68f..94ad99f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "vps-panel", "version": "0.1.0", "dependencies": { - "@types/bcryptjs": "^2.4.6", "@types/pg": "^8.20.0", "clsx": "^2.1.1", "framer-motion": "^12.40.0", @@ -1555,12 +1554,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", diff --git a/package.json b/package.json index 29714e1..2462ee0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "lint": "eslint" }, "dependencies": { - "@types/bcryptjs": "^2.4.6", "@types/pg": "^8.20.0", "clsx": "^2.1.1", "framer-motion": "^12.40.0", diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index cb0307b..4327ec5 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,13 +1,52 @@ import { NextRequest, NextResponse } from 'next/server' import { createToken, COOKIE } from '@/lib/auth' +// In-memory rate limiter: max 10 attempts per IP per 15 minutes +const attempts = new Map() +const MAX_ATTEMPTS = 10 +const WINDOW_MS = 15 * 60 * 1000 + +function getIp(req: NextRequest) { + return ( + req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? + req.headers.get('x-real-ip') ?? + '0.0.0.0' + ) +} + export async function POST(req: NextRequest) { + const ip = getIp(req) + const now = Date.now() + + const entry = attempts.get(ip) + if (entry && now < entry.resetAt) { + if (entry.count >= MAX_ATTEMPTS) { + const retryAfter = Math.ceil((entry.resetAt - now) / 1000) + return NextResponse.json( + { error: `Çok fazla deneme. ${retryAfter} saniye bekle.` }, + { status: 429, headers: { 'Retry-After': String(retryAfter) } } + ) + } + entry.count++ + } else { + attempts.set(ip, { count: 1, resetAt: now + WINDOW_MS }) + } + const { password } = await req.json() - if (password !== process.env.PANEL_SECRET) + if (password !== process.env.PANEL_SECRET) { return NextResponse.json({ error: 'Yanlış şifre' }, { status: 401 }) + } + + // Başarılı giriş — sayacı sıfırla + attempts.delete(ip) const token = await createToken() const res = NextResponse.json({ ok: true }) - res.cookies.set(COOKIE, token, { httpOnly: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, path: '/' }) + res.cookies.set(COOKIE, token, { + httpOnly: true, + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, + path: '/', + }) return res } diff --git a/src/app/api/backups/route.ts b/src/app/api/backups/route.ts index d81f261..b7a2fd5 100644 --- a/src/app/api/backups/route.ts +++ b/src/app/api/backups/route.ts @@ -1,10 +1,14 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import pool from '@/lib/appDb' import { generateId } from '@/lib/config' import { reloadBackupCrons } from '@/lib/cronWorker' +import { requireAuth } from '@/lib/auth' // GET configs and logs for a dbId -export async function GET(req: Request) { +export async function GET(req: NextRequest) { + const err = await requireAuth(req) + if (err) return err + const { searchParams } = new URL(req.url) const dbId = searchParams.get('dbId') if (!dbId) return NextResponse.json({ error: 'Missing dbId' }, { status: 400 }) @@ -23,7 +27,10 @@ export async function GET(req: Request) { } // Create or update a backup config -export async function POST(req: Request) { +export async function POST(req: NextRequest) { + const err = await requireAuth(req) + if (err) return err + try { const body = await req.json() const { db_id, schedule, cloud_type, credentials, gdrive_folder_id } = body @@ -57,7 +64,10 @@ export async function POST(req: Request) { } // Delete backup config -export async function DELETE(req: Request) { +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 diff --git a/src/app/api/backups/test/route.ts b/src/app/api/backups/test/route.ts new file mode 100644 index 0000000..5227b8a --- /dev/null +++ b/src/app/api/backups/test/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { readConfig } from '@/lib/config' +import { createDbDumpBuffer, uploadToDropbox, uploadToGoogleDrive, backupFilename, testGoogleDriveCredentials } from '@/lib/backup' +import pool from '@/lib/appDb' + +// POST /api/backups/test +// { db_id, cloud_type, credentials, gdrive_folder_id? } +// → Gerçek yedek alır, buluta yükler, loglar +export async function POST(req: NextRequest) { + const authErr = await requireAuth(req) + if (authErr) return authErr + + const { db_id, cloud_type, credentials, gdrive_folder_id } = await req.json() + + if (!credentials?.trim()) { + return NextResponse.json({ ok: false, error: 'Credentials boş olamaz.' }) + } + + // DB bilgisini al + const config = await readConfig() + const db = config.databases.find(d => d.id === db_id) + if (!db) return NextResponse.json({ ok: false, error: 'Veritabanı bulunamadı.' }) + + const filename = backupFilename() + + try { + // 1. Dump al + const buffer = await createDbDumpBuffer(db) + + // 2. Buluta yükle + if (cloud_type === 'dropbox') { + await uploadToDropbox(credentials.trim(), db.name, filename, buffer) + } else if (cloud_type === 'gdrive') { + await uploadToGoogleDrive(credentials, db.name, filename, buffer, gdrive_folder_id || undefined) + } else { + return NextResponse.json({ ok: false, error: 'Desteklenmeyen cloud türü.' }) + } + + // 3. Logla + await pool.query( + `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, + [db_id, 'success', `Manuel test yedek — ${cloud_type}`, buffer.length] + ) + + const sizeStr = buffer.length > 1024 * 1024 + ? `${(buffer.length / (1024 * 1024)).toFixed(2)} MB` + : `${(buffer.length / 1024).toFixed(1)} KB` + + return NextResponse.json({ + ok: true, + detail: `✓ Yedek alındı ve yüklendi — ${db.name}/${filename} (${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(() => {}) + + 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 e13be5d..9776a50 100644 --- a/src/app/api/db/backup/route.ts +++ b/src/app/api/db/backup/route.ts @@ -1,10 +1,14 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { readConfig } from '@/lib/config' import { createDbDumpBuffer } from '@/lib/backup' +import { requireAuth } from '@/lib/auth' import pool from '@/lib/appDb' // GET /api/db/backup?dbId=... -export async function GET(req: Request) { +export async function GET(req: NextRequest) { + const authErr = await requireAuth(req) + if (authErr) return authErr + const { searchParams } = new URL(req.url) const dbId = searchParams.get('dbId') if (!dbId) return NextResponse.json({ error: 'Missing dbId' }, { status: 400 }) @@ -18,16 +22,22 @@ export async function GET(req: Request) { const dateStr = new Date().toISOString().replace(/[:.]/g, '-') const filename = `${db.name}_${dateStr}.sql` - await pool.query(`INSERT INTO backup_logs (db_id, status, error, size, target) VALUES ($1, $2, $3, $4, $5)`, [dbId, 'success', null, buffer.length, 'manuel indirme']) + await pool.query( + `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, + [dbId, 'success', 'Manuel indirme', buffer.length] + ) - return new NextResponse(buffer.toString('utf-8'), { + return new NextResponse(new Uint8Array(buffer), { headers: { - 'Content-Type': 'application/sql', - 'Content-Disposition': `attachment; filename="${filename}"` - } + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${filename}"`, + }, }) } catch (e: any) { - await pool.query(`INSERT INTO backup_logs (db_id, status, error, size, target) VALUES ($1, $2, $3, $4, $5)`, [dbId, 'error', e.message, 0, 'manuel indirme']) + await pool.query( + `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, + [dbId, 'error', e.message, 0] + ) return NextResponse.json({ error: e.message }, { status: 500 }) } } diff --git a/src/app/api/docker/route.ts b/src/app/api/docker/route.ts index 4b53e4c..5d025b3 100644 --- a/src/app/api/docker/route.ts +++ b/src/app/api/docker/route.ts @@ -1,42 +1,112 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { readConfig } from '@/lib/config' +import { requireAuth } from '@/lib/auth' export const dynamic = 'force-dynamic' -export async function GET(req: Request) { - const { searchParams } = new URL(req.url) - const hostId = searchParams.get('hostId') +async function getDockerHost(hostId: string) { + const config = await readConfig() + return config.docker_hosts?.find(h => h.id === hostId) ?? null +} - if (!hostId) { - return NextResponse.json({ error: 'hostId is required' }, { status: 400 }) +function normalizeUrl(url: string) { + return url.endsWith('/') ? url.slice(0, -1) : url +} + +// Docker log stream parser — 8-byte frame header: [stream(1), 0, 0, 0, size(4-BE)] +function parseDockerLogs(buffer: Buffer): string { + const lines: string[] = [] + let offset = 0 + while (offset + 8 <= buffer.length) { + const size = buffer.readUInt32BE(offset + 4) + if (size === 0) { offset += 8; continue } + if (offset + 8 + size > buffer.length) break + lines.push(buffer.slice(offset + 8, offset + 8 + size).toString('utf-8').replace(/\n$/, '')) + offset += 8 + size } + return lines.join('\n') +} + +// GET /api/docker?hostId=xxx → list containers +// GET /api/docker?hostId=xxx&containerId=xxx&action=logs → container logs +export async function GET(req: NextRequest) { + const authErr = await requireAuth(req) + if (authErr) return authErr + + const { searchParams } = req.nextUrl + const hostId = searchParams.get('hostId') + const containerId = searchParams.get('containerId') + const action = searchParams.get('action') + + if (!hostId) return NextResponse.json({ error: 'hostId gerekli' }, { status: 400 }) + + const host = await getDockerHost(hostId) + if (!host) return NextResponse.json({ error: 'Docker host bulunamadı' }, { status: 404 }) + + const base = normalizeUrl(host.url) try { - const config = await readConfig() - const host = config.docker_hosts?.find(h => h.id === hostId) - - if (!host) { - return NextResponse.json({ error: 'Docker host not found' }, { status: 404 }) + if (action === 'logs' && containerId) { + const res = await fetch(`${base}/containers/${containerId}/logs?tail=200&stdout=1&stderr=1`, { + headers: { Accept: 'application/octet-stream' }, + cache: 'no-store', + }) + if (!res.ok) return NextResponse.json({ error: `Docker API Error: ${res.statusText}` }, { status: res.status }) + const buf = Buffer.from(await res.arrayBuffer()) + return NextResponse.json({ logs: parseDockerLogs(buf) }) } - let baseUrl = host.url - if (baseUrl.endsWith('/')) baseUrl = baseUrl.slice(0, -1) - - // Docker Engine API: list all containers - const res = await fetch(`${baseUrl}/containers/json?all=1`, { - headers: { 'Accept': 'application/json' }, - cache: 'no-store' + // List containers + const res = await fetch(`${base}/containers/json?all=1`, { + headers: { Accept: 'application/json' }, + cache: 'no-store', }) - if (!res.ok) { const text = await res.text() return NextResponse.json({ error: `Docker API Error: ${res.statusText}`, details: text }, { status: res.status }) } + return NextResponse.json(await res.json()) + } catch (e: any) { + return NextResponse.json({ error: 'Bağlantı hatası', details: e.message }, { status: 500 }) + } +} - const containers = await res.json() - return NextResponse.json(containers) - } catch (error: any) { - console.error('Docker API Error:', error) - return NextResponse.json({ error: 'Bağlantı hatası', details: error.message }, { status: 500 }) +// POST /api/docker { hostId, containerId, action: 'start'|'stop'|'restart' } +export async function POST(req: NextRequest) { + const authErr = await requireAuth(req) + if (authErr) return authErr + + const body = await req.json() + const { hostId, containerId, action } = body + + if (!hostId || !containerId || !action) { + return NextResponse.json({ error: 'hostId, containerId ve action gerekli' }, { status: 400 }) + } + + const validActions = ['start', 'stop', 'restart'] + if (!validActions.includes(action)) { + return NextResponse.json({ error: 'Geçersiz action' }, { status: 400 }) + } + + const host = await getDockerHost(hostId) + if (!host) return NextResponse.json({ error: 'Docker host bulunamadı' }, { status: 404 }) + + const base = normalizeUrl(host.url) + + try { + const res = await fetch(`${base}/containers/${containerId}/${action}`, { + method: 'POST', + cache: 'no-store', + }) + + // 204 = success (already in desired state or action completed) + if (res.status === 204 || res.status === 200) { + return NextResponse.json({ ok: true }) + } + + const text = await res.text() + return NextResponse.json({ error: `Docker API Error: ${res.statusText}`, details: text }, { status: res.status }) + } catch (e: any) { + return NextResponse.json({ error: 'Bağlantı hatası', details: e.message }, { status: 500 }) } } diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts new file mode 100644 index 0000000..c7f6c20 --- /dev/null +++ b/src/app/api/notifications/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getNotificationSettings, saveNotificationSettings } from '@/lib/appDb' + +export async function GET(req: NextRequest) { + const err = await requireAuth(req) + if (err) return err + const settings = await getNotificationSettings() + return NextResponse.json(settings ?? { webhook_url: '', telegram_token: '', telegram_chat_id: '', enabled: true }) +} + +export async function POST(req: NextRequest) { + const err = await requireAuth(req) + if (err) return err + const body = await req.json() + await saveNotificationSettings({ + webhook_url: body.webhook_url || null, + telegram_token: body.telegram_token || null, + telegram_chat_id: body.telegram_chat_id || null, + enabled: body.enabled ?? true, + }) + return NextResponse.json({ ok: true }) +} diff --git a/src/app/dashboard/databases/page.tsx b/src/app/dashboard/databases/page.tsx index 47bc6ff..21be9fe 100644 --- a/src/app/dashboard/databases/page.tsx +++ b/src/app/dashboard/databases/page.tsx @@ -4,7 +4,7 @@ import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/com import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { AnimatedTabs } from '@/components/ui/animated-tabs' -import { Plus, X, Database as DatabaseIcon, Download, Play, Table as TableIcon, BarChart2, HardDrive, ShieldAlert, CheckCircle2 } from 'lucide-react' +import { Plus, X, Database as DatabaseIcon, Download, Play, Table as TableIcon, BarChart2, HardDrive, ShieldAlert, CheckCircle2, Loader2, FlaskConical, Copy } from 'lucide-react' type Db = { id: string; name: string; host: string; port: number; database: string; username: string; color: string; ssl: boolean; db_type: 'postgres' | 'mysql' } type Table = { table_name: string; row_count: number; size: string } @@ -250,10 +250,36 @@ export default function DatabasesPage() { )} {/* Stats detail */} - {tab === 'stats' && stats && ( - -
{JSON.stringify(stats, null, 2)}
-
+ {tab === 'stats' && ( +
+ {!stats ? ( +
Yükleniyor...
+ ) : ( + <> +
+ {[ + { label: 'Veritabanı Boyutu', value: String(stats.size), desc: 'Toplam disk kullanımı', color: 'text-accent' }, + { label: 'Aktif Bağlantı', value: String(stats.active_connections), desc: 'Şu an açık bağlantı sayısı', color: 'text-success' }, + { label: 'Tablo Sayısı', value: String(stats.table_count), desc: 'Public schema tabloları', color: 'text-purple-400' }, + ].map(s => ( + + +
{s.label}
+
{s.value}
+
{s.desc}
+
+
+ ))} +
+ + +
Ham Veri
+
{JSON.stringify(stats, null, 2)}
+
+
+ + )} +
)} {/* Backups */} @@ -399,12 +425,26 @@ function AddDbModal({ onClose, onAdded }: { onClose: () => void; onAdded: () => ) } +const CRON_PRESETS = [ + { label: 'Her gece 00:00', value: '0 0 * * *' }, + { label: 'Her 6 saatte', value: '0 */6 * * *' }, + { label: 'Her 12 saatte', value: '0 */12 * * *' }, + { label: 'Her Pazartesi', value: '0 0 * * 1' }, +] + +function extractGdriveEmail(json: string): string | null { + try { return JSON.parse(json)?.client_email ?? null } catch { return null } +} + function BackupTab({ db }: { db: Db }) { const [config, setConfig] = useState(null) const [logs, setLogs] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) - const [f, setF] = useState({ schedule: '0 0 * * *', cloud_type: 'dropbox', credentials: '', gdrive_folder_id: '' }) + const [testing, setTesting] = useState(false) + const [testResult, setTestResult] = useState<{ ok: boolean; detail?: string; error?: string } | null>(null) + const [copied, setCopied] = useState(false) + const [f, setF] = useState({ schedule: '0 0 * * *', cloud_type: 'gdrive', credentials: '', gdrive_folder_id: '' }) const load = useCallback(async () => { setLoading(true) @@ -427,105 +467,259 @@ function BackupTab({ db }: { db: Db }) { await fetch('/api/backups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ db_id: db.id, ...f }) + body: JSON.stringify({ db_id: db.id, ...f }), }) await load() setSaving(false) } + const testCredentials = async () => { + setTesting(true) + setTestResult(null) + const r = await fetch('/api/backups/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ db_id: db.id, cloud_type: f.cloud_type, credentials: f.credentials, gdrive_folder_id: f.gdrive_folder_id }), + }) + const result = await r.json() + setTestResult(result) + if (result.ok) await load() // log listesini güncelle + setTesting(false) + } + const deleteConfig = async () => { if (!confirm('Otomatik yedeklemeyi kapat?')) return - await fetch('/api/backups', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ db_id: db.id }) - }) + await fetch('/api/backups', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db_id: db.id }) }) setConfig(null) - setF({ schedule: '0 0 * * *', cloud_type: 'dropbox', credentials: '', gdrive_folder_id: '' }) + setF({ schedule: '0 0 * * *', cloud_type: 'gdrive', credentials: '', gdrive_folder_id: '' }) + } + + const serviceEmail = f.cloud_type === 'gdrive' ? extractGdriveEmail(f.credentials) : null + + const copyEmail = () => { + if (serviceEmail) { navigator.clipboard.writeText(serviceEmail); setCopied(true); setTimeout(() => setCopied(false), 2000) } } if (loading) return
Yükleniyor...
return (
- - -
- Otomatik Yedekleme - Düzenli veritabanı yedekleri alın ve buluta gönderin. -
- - - Hemen İndir - -
- -
- - setF(p => ({ ...p, schedule: e.target.value }))} placeholder="0 0 * * *" /> -
- -
- - -
+ İndir + +
+
-
- -