diff --git a/backend/src/index.ts b/backend/src/index.ts index 4861931..a26d426 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -126,6 +126,7 @@ async function start() { try { await mongoose.connect(config.mongoUri); console.log("MongoDB'ye bağlanıldı"); + await deploymentService.ensureSettings(); await jobService.bootstrapFromFilesystem(); await jobService.bootstrap(); await deploymentService.normalizeExistingCommitMessages(); diff --git a/backend/src/models/settings.ts b/backend/src/models/settings.ts index 8f0f7b1..6141f05 100644 --- a/backend/src/models/settings.ts +++ b/backend/src/models/settings.ts @@ -3,6 +3,8 @@ import mongoose, { Schema, Document } from "mongoose"; export interface SettingsDocument extends Document { webhookToken: string; webhookSecret: string; + cleanupIntervalValue?: number; + cleanupIntervalUnit?: "saat" | "gün" | "hafta"; createdAt: Date; updatedAt: Date; } @@ -10,7 +12,9 @@ export interface SettingsDocument extends Document { const SettingsSchema = new Schema( { webhookToken: { type: String, required: true }, - webhookSecret: { type: String, required: true } + webhookSecret: { type: String, required: true }, + cleanupIntervalValue: { type: Number, min: 1 }, + cleanupIntervalUnit: { type: String, enum: ["saat", "gün", "hafta"] } }, { timestamps: true } ); diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index a9f6415..bbcde58 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -11,6 +11,8 @@ router.get("/", async (_req, res) => { return res.json({ webhookToken: settings.webhookToken, webhookSecret: settings.webhookSecret, + cleanupIntervalValue: settings.cleanupIntervalValue, + cleanupIntervalUnit: settings.cleanupIntervalUnit, updatedAt: settings.updatedAt }); }); @@ -31,4 +33,29 @@ router.post("/secret/rotate", async (_req, res) => { }); }); +router.post("/cleanup-interval", async (req, res) => { + const settings = await deploymentService.ensureSettings(); + const { value, unit } = req.body as { + value?: number; + unit?: "saat" | "gün" | "hafta"; + }; + if (!value || value < 1 || !unit) { + return res.status(400).json({ message: "Geçerli periyot gerekli" }); + } + settings.cleanupIntervalValue = value; + settings.cleanupIntervalUnit = unit; + await settings.save(); + await deploymentService.updateCleanupSchedule(value, unit); + return res.json({ + cleanupIntervalValue: settings.cleanupIntervalValue, + cleanupIntervalUnit: settings.cleanupIntervalUnit, + updatedAt: settings.updatedAt + }); +}); + +router.post("/cleanup-images", async (_req, res) => { + await deploymentService.cleanupUnusedImages(); + return res.json({ success: true }); +}); + export default router; diff --git a/backend/src/services/deploymentService.ts b/backend/src/services/deploymentService.ts index 7eb0d48..909c68d 100644 --- a/backend/src/services/deploymentService.ts +++ b/backend/src/services/deploymentService.ts @@ -41,6 +41,8 @@ type DeploymentMetadata = { type SettingsMetadata = { webhookToken: string; webhookSecret: string; + cleanupIntervalValue?: number; + cleanupIntervalUnit?: "saat" | "gün" | "hafta"; }; type StoredRun = { @@ -360,6 +362,7 @@ async function runCompose(project: DeploymentProjectDocument, onData: (line: str class DeploymentService { private running: Map = new Map(); private io: Server | null = null; + private cleanupTimer: NodeJS.Timeout | null = null; setSocket(io: Server) { this.io = io; @@ -451,14 +454,23 @@ class DeploymentService { async ensureSettings() { const existing = await Settings.findOne(); - if (existing) return existing; + if (existing) { + await this.updateCleanupSchedule(existing.cleanupIntervalValue, existing.cleanupIntervalUnit); + return existing; + } const fileSettings = await readSettingsFile(); if (fileSettings) { const createdFromFile = await Settings.create({ webhookToken: fileSettings.webhookToken, - webhookSecret: fileSettings.webhookSecret + webhookSecret: fileSettings.webhookSecret, + cleanupIntervalValue: fileSettings.cleanupIntervalValue, + cleanupIntervalUnit: fileSettings.cleanupIntervalUnit }); + await this.updateCleanupSchedule( + createdFromFile.cleanupIntervalValue, + createdFromFile.cleanupIntervalUnit + ); return createdFromFile; } @@ -468,8 +480,11 @@ class DeploymentService { }); await writeSettingsFile({ webhookToken: created.webhookToken, - webhookSecret: created.webhookSecret + webhookSecret: created.webhookSecret, + cleanupIntervalValue: created.cleanupIntervalValue, + cleanupIntervalUnit: created.cleanupIntervalUnit }); + await this.updateCleanupSchedule(created.cleanupIntervalValue, created.cleanupIntervalUnit); return created; } @@ -479,7 +494,9 @@ class DeploymentService { await settings.save(); await writeSettingsFile({ webhookToken: settings.webhookToken, - webhookSecret: settings.webhookSecret + webhookSecret: settings.webhookSecret, + cleanupIntervalValue: settings.cleanupIntervalValue, + cleanupIntervalUnit: settings.cleanupIntervalUnit }); return settings; } @@ -490,11 +507,35 @@ class DeploymentService { await settings.save(); await writeSettingsFile({ webhookToken: settings.webhookToken, - webhookSecret: settings.webhookSecret + webhookSecret: settings.webhookSecret, + cleanupIntervalValue: settings.cleanupIntervalValue, + cleanupIntervalUnit: settings.cleanupIntervalUnit }); return settings; } + async updateCleanupSchedule(value?: number, unit?: "saat" | "gün" | "hafta") { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + if (!value || !unit) return; + const intervalMs = + unit === "saat" + ? value * 60 * 60 * 1000 + : unit === "gün" + ? value * 24 * 60 * 60 * 1000 + : value * 7 * 24 * 60 * 60 * 1000; + if (!intervalMs || Number.isNaN(intervalMs)) return; + this.cleanupTimer = setInterval(() => { + this.cleanupUnusedImages().catch(() => undefined); + }, intervalMs); + } + + async cleanupUnusedImages() { + await runCommand("docker image prune -a -f", process.cwd(), () => undefined); + } + async createProject(input: { name: string; repoUrl: string; diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts index 685f47d..4a07b97 100644 --- a/frontend/src/api/settings.ts +++ b/frontend/src/api/settings.ts @@ -3,6 +3,8 @@ import { apiClient } from "./client"; export interface SettingsResponse { webhookToken: string; webhookSecret: string; + cleanupIntervalValue?: number; + cleanupIntervalUnit?: "saat" | "gün" | "hafta"; updatedAt: string; } @@ -20,3 +22,13 @@ export async function rotateWebhookSecret(): Promise { const { data } = await apiClient.post("/settings/secret/rotate"); return data as SettingsResponse; } + +export async function saveCleanupInterval(value: number, unit: "saat" | "gün" | "hafta") { + const { data } = await apiClient.post("/settings/cleanup-interval", { value, unit }); + return data as SettingsResponse; +} + +export async function cleanupImages(): Promise<{ success: boolean }> { + const { data } = await apiClient.post("/settings/cleanup-images"); + return data as { success: boolean }; +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index d4baba6..941a2bd 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,10 +1,20 @@ import { useEffect, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCopy, faEye, faEyeSlash, faRotate } from "@fortawesome/free-solid-svg-icons"; +import { faBroom, faCopy, faEye, faEyeSlash, faFloppyDisk, faRotate } from "@fortawesome/free-solid-svg-icons"; import { toast } from "sonner"; import { Button } from "../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; -import { fetchSettings, rotateWebhookSecret, rotateWebhookToken, SettingsResponse } from "../api/settings"; +import { Input } from "../components/ui/input"; +import { Label } from "../components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select"; +import { + cleanupImages, + fetchSettings, + rotateWebhookSecret, + rotateWebhookToken, + saveCleanupInterval, + SettingsResponse +} from "../api/settings"; export function SettingsPage() { const [settings, setSettings] = useState(null); @@ -13,10 +23,22 @@ export function SettingsPage() { const [rotatingSecret, setRotatingSecret] = useState(false); const [showToken, setShowToken] = useState(false); const [showSecret, setShowSecret] = useState(false); + const [cleanupValue, setCleanupValue] = useState("1"); + const [cleanupUnit, setCleanupUnit] = useState<"saat" | "gün" | "hafta">("hafta"); + const [savingCleanup, setSavingCleanup] = useState(false); + const [cleaning, setCleaning] = useState(false); useEffect(() => { fetchSettings() - .then((data) => setSettings(data)) + .then((data) => { + setSettings(data); + if (data.cleanupIntervalValue) { + setCleanupValue(String(data.cleanupIntervalValue)); + } + if (data.cleanupIntervalUnit) { + setCleanupUnit(data.cleanupIntervalUnit); + } + }) .catch(() => toast.error("Settings yüklenemedi")) .finally(() => setLoading(false)); }, []); @@ -77,6 +99,36 @@ export function SettingsPage() { } }; + const handleSaveCleanup = async () => { + const value = Number(cleanupValue); + if (!value || Number.isNaN(value) || value < 1) { + toast.error("Geçerli bir periyot girin"); + return; + } + setSavingCleanup(true); + try { + const data = await saveCleanupInterval(value, cleanupUnit); + setSettings((prev) => (prev ? { ...prev, ...data } : data)); + toast.success("Temizlik periyodu kaydedildi"); + } catch { + toast.error("Periyot kaydedilemedi"); + } finally { + setSavingCleanup(false); + } + }; + + const handleCleanupImages = async () => { + setCleaning(true); + try { + await cleanupImages(); + toast.success("Kullanılmayan image'lar temizlendi"); + } catch { + toast.error("Temizlik başarısız"); + } finally { + setCleaning(false); + } + }; + if (loading) { return (
@@ -138,6 +190,58 @@ export function SettingsPage() { + + + Image Temizliği + + +
+
+ +
+ setCleanupValue(e.target.value)} + className="bg-white" + /> + +
+
+
+ + +
+
+
+
+ Webhook Secret