Compare commits

...

2 Commits

Author SHA1 Message Date
064a04d898 refactor(settings): ayarlar sayfası düzenini güncelle 2026-02-03 09:37:09 +00:00
1f90ce54d4 feat(settings): otomatik docker image temizliği ekle
Docker image temizliği için yapılandırılabilir zamanlayıcı ve manuel
tetikleme özelliği eklenmiştir. Kullanıcılar saat, gün veya hafta bazlı
periyotlar belirleyebilir ve anlık temizlik yapabilir.
2026-02-03 09:34:37 +00:00
6 changed files with 198 additions and 9 deletions

View File

@@ -126,6 +126,7 @@ async function start() {
try { try {
await mongoose.connect(config.mongoUri); await mongoose.connect(config.mongoUri);
console.log("MongoDB'ye bağlanıldı"); console.log("MongoDB'ye bağlanıldı");
await deploymentService.ensureSettings();
await jobService.bootstrapFromFilesystem(); await jobService.bootstrapFromFilesystem();
await jobService.bootstrap(); await jobService.bootstrap();
await deploymentService.normalizeExistingCommitMessages(); await deploymentService.normalizeExistingCommitMessages();

View File

@@ -3,6 +3,8 @@ import mongoose, { Schema, Document } from "mongoose";
export interface SettingsDocument extends Document { export interface SettingsDocument extends Document {
webhookToken: string; webhookToken: string;
webhookSecret: string; webhookSecret: string;
cleanupIntervalValue?: number;
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
@@ -10,7 +12,9 @@ export interface SettingsDocument extends Document {
const SettingsSchema = new Schema<SettingsDocument>( const SettingsSchema = new Schema<SettingsDocument>(
{ {
webhookToken: { type: String, required: true }, 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 } { timestamps: true }
); );

View File

@@ -11,6 +11,8 @@ router.get("/", async (_req, res) => {
return res.json({ return res.json({
webhookToken: settings.webhookToken, webhookToken: settings.webhookToken,
webhookSecret: settings.webhookSecret, webhookSecret: settings.webhookSecret,
cleanupIntervalValue: settings.cleanupIntervalValue,
cleanupIntervalUnit: settings.cleanupIntervalUnit,
updatedAt: settings.updatedAt 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; export default router;

View File

@@ -41,6 +41,8 @@ type DeploymentMetadata = {
type SettingsMetadata = { type SettingsMetadata = {
webhookToken: string; webhookToken: string;
webhookSecret: string; webhookSecret: string;
cleanupIntervalValue?: number;
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
}; };
type StoredRun = { type StoredRun = {
@@ -360,6 +362,7 @@ async function runCompose(project: DeploymentProjectDocument, onData: (line: str
class DeploymentService { class DeploymentService {
private running: Map<string, boolean> = new Map(); private running: Map<string, boolean> = new Map();
private io: Server | null = null; private io: Server | null = null;
private cleanupTimer: NodeJS.Timeout | null = null;
setSocket(io: Server) { setSocket(io: Server) {
this.io = io; this.io = io;
@@ -451,14 +454,23 @@ class DeploymentService {
async ensureSettings() { async ensureSettings() {
const existing = await Settings.findOne(); const existing = await Settings.findOne();
if (existing) return existing; if (existing) {
await this.updateCleanupSchedule(existing.cleanupIntervalValue, existing.cleanupIntervalUnit);
return existing;
}
const fileSettings = await readSettingsFile(); const fileSettings = await readSettingsFile();
if (fileSettings) { if (fileSettings) {
const createdFromFile = await Settings.create({ const createdFromFile = await Settings.create({
webhookToken: fileSettings.webhookToken, webhookToken: fileSettings.webhookToken,
webhookSecret: fileSettings.webhookSecret webhookSecret: fileSettings.webhookSecret,
cleanupIntervalValue: fileSettings.cleanupIntervalValue,
cleanupIntervalUnit: fileSettings.cleanupIntervalUnit
}); });
await this.updateCleanupSchedule(
createdFromFile.cleanupIntervalValue,
createdFromFile.cleanupIntervalUnit
);
return createdFromFile; return createdFromFile;
} }
@@ -468,8 +480,11 @@ class DeploymentService {
}); });
await writeSettingsFile({ await writeSettingsFile({
webhookToken: created.webhookToken, webhookToken: created.webhookToken,
webhookSecret: created.webhookSecret webhookSecret: created.webhookSecret,
cleanupIntervalValue: created.cleanupIntervalValue,
cleanupIntervalUnit: created.cleanupIntervalUnit
}); });
await this.updateCleanupSchedule(created.cleanupIntervalValue, created.cleanupIntervalUnit);
return created; return created;
} }
@@ -479,7 +494,9 @@ class DeploymentService {
await settings.save(); await settings.save();
await writeSettingsFile({ await writeSettingsFile({
webhookToken: settings.webhookToken, webhookToken: settings.webhookToken,
webhookSecret: settings.webhookSecret webhookSecret: settings.webhookSecret,
cleanupIntervalValue: settings.cleanupIntervalValue,
cleanupIntervalUnit: settings.cleanupIntervalUnit
}); });
return settings; return settings;
} }
@@ -490,11 +507,35 @@ class DeploymentService {
await settings.save(); await settings.save();
await writeSettingsFile({ await writeSettingsFile({
webhookToken: settings.webhookToken, webhookToken: settings.webhookToken,
webhookSecret: settings.webhookSecret webhookSecret: settings.webhookSecret,
cleanupIntervalValue: settings.cleanupIntervalValue,
cleanupIntervalUnit: settings.cleanupIntervalUnit
}); });
return settings; 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: { async createProject(input: {
name: string; name: string;
repoUrl: string; repoUrl: string;

View File

@@ -3,6 +3,8 @@ import { apiClient } from "./client";
export interface SettingsResponse { export interface SettingsResponse {
webhookToken: string; webhookToken: string;
webhookSecret: string; webhookSecret: string;
cleanupIntervalValue?: number;
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
updatedAt: string; updatedAt: string;
} }
@@ -20,3 +22,13 @@ export async function rotateWebhookSecret(): Promise<SettingsResponse> {
const { data } = await apiClient.post("/settings/secret/rotate"); const { data } = await apiClient.post("/settings/secret/rotate");
return data as SettingsResponse; 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 };
}

View File

@@ -1,10 +1,20 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { toast } from "sonner";
import { Button } from "../components/ui/button"; import { Button } from "../components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; 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() { export function SettingsPage() {
const [settings, setSettings] = useState<SettingsResponse | null>(null); const [settings, setSettings] = useState<SettingsResponse | null>(null);
@@ -13,10 +23,22 @@ export function SettingsPage() {
const [rotatingSecret, setRotatingSecret] = useState(false); const [rotatingSecret, setRotatingSecret] = useState(false);
const [showToken, setShowToken] = useState(false); const [showToken, setShowToken] = useState(false);
const [showSecret, setShowSecret] = useState(false); const [showSecret, setShowSecret] = useState(false);
const [cleanupValue, setCleanupValue] = useState<string>("1");
const [cleanupUnit, setCleanupUnit] = useState<"saat" | "gün" | "hafta">("hafta");
const [savingCleanup, setSavingCleanup] = useState(false);
const [cleaning, setCleaning] = useState(false);
useEffect(() => { useEffect(() => {
fetchSettings() 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")) .catch(() => toast.error("Settings yüklenemedi"))
.finally(() => setLoading(false)); .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) { if (loading) {
return ( return (
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground"> <div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
@@ -173,6 +225,58 @@ export function SettingsPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Image Temizliği</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
<div className="space-y-2">
<Label htmlFor="cleanupValue">Temizlik Periyodu</Label>
<div className="flex items-center gap-2">
<Input
id="cleanupValue"
type="number"
min="1"
value={cleanupValue}
onChange={(e) => setCleanupValue(e.target.value)}
className="bg-white"
/>
<Select value={cleanupUnit} onValueChange={(value) => setCleanupUnit(value as typeof cleanupUnit)}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Birim" />
</SelectTrigger>
<SelectContent>
<SelectItem value="saat">saat</SelectItem>
<SelectItem value="gün">gün</SelectItem>
<SelectItem value="hafta">hafta</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
<Button
variant="outline"
onClick={handleSaveCleanup}
disabled={savingCleanup}
className="gap-2 bg-white text-foreground hover:bg-muted"
>
<FontAwesomeIcon icon={faFloppyDisk} className="h-4 w-4" />
Kaydet
</Button>
<Button
onClick={handleCleanupImages}
disabled={cleaning}
className="gap-2 bg-black text-white hover:bg-black/90"
>
<FontAwesomeIcon icon={faBroom} className="h-4 w-4" />
Clean Cache Images
</Button>
</div>
</div>
</CardContent>
</Card>
</div> </div>
); );
} }