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 ( -
Cookie-free, self-hosted — Plausible benzeri
+Cookie-free, self-hosted website tracking
Sitelerine tracking snippet ekle, veriler burada görünecek.
+ Snippet'ı Göster
+ <head> tagının içine ekle:
- {''} tagının içine ekle:
+
+ {snippet}
+
-
- {snippet}
-
+ | {h} | )}||||||
|---|---|---|---|---|---|---|
| {t.table_name} | -{Number(t.row_count).toLocaleString()} | -{t.size} | + ?
| + {h} + | + ))}
|---|