From 29d03604f9ffc6b64aed2211db5a6330dafbb8f4 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Wed, 3 Jun 2026 00:58:54 +0300 Subject: [PATCH] feat: add docker monitoring and ui fixes --- data/panel.db | 0 package-lock.json | 192 ++++++++- package.json | 6 +- src/app/api/backups/route.ts | 10 +- src/app/api/db/backup/route.ts | 26 +- src/app/api/docker/route.ts | 42 ++ src/app/dashboard/analytics/page.tsx | 425 +++++++++++--------- src/app/dashboard/databases/page.tsx | 501 ++++++++++++++++-------- src/app/dashboard/docker/ClientPage.tsx | 240 ++++++++++++ src/app/dashboard/docker/page.tsx | 9 + src/app/globals.css | 45 ++- src/app/page.tsx | 83 +++- src/components/Sidebar.tsx | 73 ++-- src/components/ui/animated-tabs.tsx | 46 +++ src/components/ui/button.tsx | 38 ++ src/components/ui/card.tsx | 69 ++++ src/components/ui/grid-pattern.tsx | 71 ++++ src/components/ui/input.tsx | 23 ++ src/lib/appDb.ts | 14 +- src/lib/backup.ts | 73 ++-- src/lib/config.ts | 34 +- src/lib/cronWorker.ts | 17 +- src/lib/db.ts | 192 ++++++--- src/lib/utils.ts | 6 + 24 files changed, 1682 insertions(+), 553 deletions(-) create mode 100644 data/panel.db create mode 100644 src/app/api/docker/route.ts create mode 100644 src/app/dashboard/docker/ClientPage.tsx create mode 100644 src/app/dashboard/docker/page.tsx create mode 100644 src/components/ui/animated-tabs.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/grid-pattern.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/lib/utils.ts diff --git a/data/panel.db b/data/panel.db new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index ce00e5c..bc7b68f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,17 @@ "dependencies": { "@types/bcryptjs": "^2.4.6", "@types/pg": "^8.20.0", + "clsx": "^2.1.1", + "framer-motion": "^12.40.0", "jose": "^6.2.3", "lucide-react": "^1.16.0", + "mysql2": "^3.22.4", "next": "16.2.6", "node-cron": "^4.2.1", "pg": "^8.21.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -2499,6 +2503,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", @@ -2699,6 +2712,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2877,6 +2899,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3690,6 +3721,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3731,6 +3789,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -3995,6 +4062,22 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4317,6 +4400,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4924,6 +5013,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4947,6 +5042,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide-react": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", @@ -5023,6 +5133,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5030,6 +5155,40 @@ "dev": true, "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.4.tgz", + "integrity": "sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -5866,6 +6025,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6106,6 +6271,21 @@ "node": ">= 10.x" } }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6312,6 +6492,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", diff --git a/package.json b/package.json index 34d41f5..29714e1 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,17 @@ "dependencies": { "@types/bcryptjs": "^2.4.6", "@types/pg": "^8.20.0", + "clsx": "^2.1.1", + "framer-motion": "^12.40.0", "jose": "^6.2.3", "lucide-react": "^1.16.0", + "mysql2": "^3.22.4", "next": "16.2.6", "node-cron": "^4.2.1", "pg": "^8.21.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/app/api/backups/route.ts b/src/app/api/backups/route.ts index 63bd444..d81f261 100644 --- a/src/app/api/backups/route.ts +++ b/src/app/api/backups/route.ts @@ -26,7 +26,7 @@ export async function GET(req: Request) { export async function POST(req: Request) { try { const body = await req.json() - const { db_id, schedule, cloud_type, credentials } = body + const { db_id, schedule, cloud_type, credentials, gdrive_folder_id } = body if (!db_id || !schedule || !cloud_type || !credentials) { return NextResponse.json({ error: 'Missing fields' }, { status: 400 }) @@ -37,13 +37,13 @@ export async function POST(req: Request) { if (existing.rows.length > 0) { await pool.query( - `UPDATE backup_configs SET schedule=$1, cloud_type=$2, credentials=$3 WHERE db_id=$4`, - [schedule, cloud_type, credentials, db_id] + `UPDATE backup_configs SET schedule=$1, cloud_type=$2, credentials=$3, gdrive_folder_id=$4 WHERE db_id=$5`, + [schedule, cloud_type, credentials, gdrive_folder_id || null, db_id] ) } else { await pool.query( - `INSERT INTO backup_configs (id, db_id, schedule, cloud_type, credentials) VALUES ($1, $2, $3, $4, $5)`, - [generateId(), db_id, schedule, cloud_type, credentials] + `INSERT INTO backup_configs (id, db_id, schedule, cloud_type, credentials, gdrive_folder_id) VALUES ($1, $2, $3, $4, $5, $6)`, + [generateId(), db_id, schedule, cloud_type, credentials, gdrive_folder_id || null] ) } diff --git a/src/app/api/db/backup/route.ts b/src/app/api/db/backup/route.ts index 7c37b2d..e13be5d 100644 --- a/src/app/api/db/backup/route.ts +++ b/src/app/api/db/backup/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server' import { readConfig } from '@/lib/config' -import { createPgDumpStream } from '@/lib/backup' +import { createDbDumpBuffer } from '@/lib/backup' +import pool from '@/lib/appDb' // GET /api/db/backup?dbId=... export async function GET(req: Request) { @@ -13,35 +14,20 @@ export async function GET(req: Request) { if (!db) return NextResponse.json({ error: 'Database not found' }, { status: 404 }) try { - const child = await createPgDumpStream(db) + const buffer = await createDbDumpBuffer(db) const dateStr = new Date().toISOString().replace(/[:.]/g, '-') const filename = `${db.name}_${dateStr}.sql` - // Stream the output of pg_dump to the response - const stream = new ReadableStream({ - start(controller) { - child.stdout.on('data', (chunk) => controller.enqueue(chunk)) - child.on('close', (code) => { - if (code === 0) { - controller.close() - } else { - controller.error(new Error(`pg_dump exited with code ${code}`)) - } - }) - child.on('error', (err) => controller.error(err)) - }, - cancel() { - child.kill() - } - }) + 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']) - return new NextResponse(stream, { + return new NextResponse(buffer.toString('utf-8'), { headers: { 'Content-Type': 'application/sql', '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']) return NextResponse.json({ error: e.message }, { status: 500 }) } } diff --git a/src/app/api/docker/route.ts b/src/app/api/docker/route.ts new file mode 100644 index 0000000..4b53e4c --- /dev/null +++ b/src/app/api/docker/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server' +import { readConfig } from '@/lib/config' + +export const dynamic = 'force-dynamic' + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url) + const hostId = searchParams.get('hostId') + + if (!hostId) { + return NextResponse.json({ error: 'hostId is required' }, { status: 400 }) + } + + 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 }) + } + + 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' + }) + + if (!res.ok) { + const text = await res.text() + return NextResponse.json({ error: `Docker API Error: ${res.statusText}`, details: text }, { status: res.status }) + } + + 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 }) + } +} diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx index fdf229b..fdb1c24 100644 --- a/src/app/dashboard/analytics/page.tsx +++ b/src/app/dashboard/analytics/page.tsx @@ -1,5 +1,10 @@ 'use client' import { useState, useEffect, useCallback } from 'react' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +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' type Stats = { total: number @@ -14,29 +19,29 @@ type Stats = { } const DAYS_OPTIONS = [ - { label: '1g', value: 1 }, - { label: '7g', value: 7 }, - { label: '30g', value: 30 }, - { label: '90g', value: 90 }, + { id: '1', label: '1G' }, + { id: '7', label: '7G' }, + { id: '30', label: '30G' }, + { id: '90', label: '90G' }, ] -function Bar({ label, value, max, color = 'var(--accent)' }: { label: string; value: number; max: number; color?: string }) { +function Bar({ label, value, max, color = 'var(--color-accent)' }: { 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
Henüz veri yok
// Tüm günleri doldur const filled: { day: string; views: number; visitors: number }[] = [] @@ -52,11 +57,19 @@ 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) => ( -
-
0 ? 4 : 0)}%`, transition: 'height .3s ease', minHeight: d.views > 0 ? 2 : 0 }} /> +
+
0 ? 4 : 0)}%`, + minHeight: d.views > 0 ? 2 : 0 + }} + />
))}
@@ -66,7 +79,7 @@ function MiniChart({ data, days }: { data: { day: string; views: number; visitor export default function AnalyticsPage() { const [domains, setDomains] = useState([]) const [selectedDomain, setSelectedDomain] = useState(null) - const [days, setDays] = useState(30) + const [days, setDays] = useState('30') const [stats, setStats] = useState(null) const [loading, setLoading] = useState(false) const [showSnippet, setShowSnippet] = useState(false) @@ -118,7 +131,6 @@ export default function AnalyticsPage() { }) setSelectedDomain(newName.trim()) - // Refresh domains after brief delay for state to sync setTimeout(loadDomains, 100) } @@ -137,236 +149,275 @@ export default function AnalyticsPage() { ` return ( -
+
{/* Header */} -
+
-

Analytics

-

Cookie-free, self-hosted — Plausible benzeri

+

Analytics

+

Cookie-free, self-hosted website tracking

-
+ +
+ {/* Domain Actions */} {selectedDomain && ( - <> - - - +
+ +
+ +
)} - - - {/* Days filter */} -
- {DAYS_OPTIONS.map(opt => ( - - ))} + + + + + + {/* 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.
- -
+ + + +

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: 'var(--accent)' }, - { label: 'Tekil Ziyaretçi', value: stats.unique.toLocaleString(), color: 'var(--green)' }, + { 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
-
+ + +
{s.label}
+
{s.value}
+
+ son {days} gün +
+
+
))}
{/* Chart */} -
-
Günlük Görüntülemeler
- -
- {days} gün önce - bugün -
-
+ + + 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 => ) - } -
+ + + 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 - }) - } -
+ + + 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 => ) - } -
+ + + Ü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 => ) - } -
+ + + {section.title} + + + {section.data.length === 0 + ?
Veri yok
+ : section.data.map(d => ) + } +
+
))}
- +
)} {loading && ( -
Yükleniyor...
+
Yükleniyor...
)} {/* Snippet modal */} {showSnippet && ( -
setShowSnippet(false)}> -
e.stopPropagation()}> -
Tracking Snippet
-
- Sitendeki <head> tagının içine ekle: -
+
setShowSnippet(false)}> + e.stopPropagation()}> + + Tracking Snippet + + Sitendeki {''} tagının içine ekle: + + + +
+                {snippet}
+              
-
-              {snippet}
-            
+
+
data-domain — hangi domain olduğunu belirtir, değiştirme
+
defer — sayfayı yavaşlatmaz, arka planda yüklenir
+
-
- data-domain — hangi domain olduğunu belirtir, değiştirme -
-
- defer — sayfayı yavaşlatmaz, arka planda yüklenir -
- -
- - -
-
+
+ + +
+ +
)} {/* 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)} - 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()); - }) - } - }} - /> - -
- - -
-
+ /> + +
+ + +
+ +
)}
) } - diff --git a/src/app/dashboard/databases/page.tsx b/src/app/dashboard/databases/page.tsx index 78e3d7d..47bc6ff 100644 --- a/src/app/dashboard/databases/page.tsx +++ b/src/app/dashboard/databases/page.tsx @@ -1,7 +1,12 @@ 'use client' import { useState, useEffect, useCallback } from 'react' +import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card' +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' -type Db = { id: string; name: string; host: string; port: number; database: string; username: string; color: string; ssl: boolean } +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 } type QueryResult = { rows: Record[]; fields: string[]; error: string | null } @@ -50,25 +55,38 @@ export default function DatabasesPage() { } return ( -
+
{/* LEFT */} -
-
- Databases - +
+
+ Databases +
-
+
{dbs.length === 0 - ?
Henüz DB yok
+ ?
Henüz DB yok
: dbs.map(db => ( -
select(db)} style={{ padding: '10px 12px', borderRadius: 8, cursor: 'pointer', marginBottom: 4, background: selected?.id === db.id ? 'rgba(255,255,255,.04)' : 'transparent', border: `1px solid ${selected?.id === db.id ? 'var(--border)' : 'transparent'}` }}> -
-
- {db.name} - +
select(db)} + className={`p-3 rounded-xl cursor-pointer transition-all border ${ + selected?.id === db.id + ? 'bg-accent/5 border-accent/20' + : 'border-transparent hover:bg-surface-2 hover:border-border/50' + }`} + > +
+
+ {db.name} +
-
- {db.host}/{db.database} +
+ {db.host}/{db.database} + {db.db_type === 'mysql' ? 'MY' : 'PG'}
))} @@ -76,90 +94,149 @@ export default function DatabasesPage() {
{/* RIGHT */} -
+
{!selected - ?
← DB seç
+ ? ( +
+ + Sol menüden bir veritabanı seçin +
+ ) : ( -
-
-
-
-

{selected.name}

+
+
+
+
+

{selected.name}

+
+
+ {selected.username}@{selected.host}:{selected.port}/{selected.database}
-
{selected.username}@{selected.host}:{selected.port}/{selected.database}
{/* Stats row */} {stats && ( -
+
{[ - { l: 'Boyut', v: String(stats.size) }, - { l: 'Aktif Bağlantı', v: String(stats.active_connections) }, - { l: 'Tablo Sayısı', v: String(stats.table_count) }, + { l: 'Boyut', v: String(stats.size), icon: HardDrive }, + { l: 'Aktif Bağlantı', v: String(stats.active_connections), icon: ActivityIcon }, + { l: 'Tablo Sayısı', v: String(stats.table_count), icon: TableIcon }, ].map(s => ( -
-
{s.l}
-
{s.v}
-
+ + +
+
{s.l}
+
{s.v}
+
+ +
+
))}
)} {/* Tabs */} -
- {(['tables', 'query', 'stats', 'backups'] as const).map(t => ( - - ))} +
+ setTab(t)} + tabs={[ + { id: 'tables', label: 'Tablolar', icon: }, + { id: 'query', label: 'Sorgu', icon: }, + { id: 'stats', label: 'İstatistik', icon: }, + { id: 'backups', label: 'Yedekler', icon: }, + ]} + />
{/* Tables */} {tab === 'tables' && ( -
+ {tables.length === 0 - ?
Tablo bulunamadı veya yükleniyor...
- : - {['Tablo','Satır','Boyut'].map(h => )} - - {tables.map(t => ( - { setTab('query'); setSql(`SELECT * FROM ${t.table_name} LIMIT 50`) }}> - - - + ?
Tablo bulunamadı veya yükleniyor...
+ :
+
{h}
{t.table_name}{Number(t.row_count).toLocaleString()}{t.size}
+ + + {['Tablo','Satır','Boyut'].map(h => ( + + ))} - ))} - -
+ {h} +
+ + + {tables.map((t, i) => ( + { setTab('query'); setSql(`SELECT * FROM ${t.table_name} LIMIT 50`) }} + className="group cursor-pointer hover:bg-white/[0.02] transition-colors" + > + + {t.table_name} + + + {Number(t.row_count).toLocaleString()} + + + {t.size} + + + ))} + + +
} -
+ )} {/* Query */} {tab === 'query' && ( -
-