From 584d6cc319187e1e20ea7bc9ecbddaf21f827bab Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 5 Jan 2026 21:27:05 +0300 Subject: [PATCH] =?UTF-8?q?feat(loop):=20ayn=C4=B1=20torrent=20i=C3=A7in?= =?UTF-8?q?=20birden=20fazla=20i=C5=9F=20deste=C4=9Fi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aynı torrent hash'i için oluşturulan yeni loop işleri, mevcut aktif işleri otomatik olarak durdurur. Stop-by-hash endpoint'i tüm ilgili işleri durduracak şekilde güncellendi. TorrentTable bileşeni çoklu işleri doğru şekilde işleyecek ve profil adını en güncel aktif işten alacak şekilde yeniden yazıldı. LoopJob arayüzüne createdAt ve updatedAt alanları eklendi. --- apps/server/src/loop/loop.engine.ts | 10 ++++++ apps/server/src/loop/loop.routes.ts | 14 +++++--- apps/server/src/utils/validators.ts | 2 ++ .../web/src/components/loop/LoopSetupCard.tsx | 20 +++++++++++ .../src/components/torrents/TorrentTable.tsx | 35 +++++++++++++------ apps/web/src/store/useAppStore.ts | 2 ++ 6 files changed, 68 insertions(+), 15 deletions(-) diff --git a/apps/server/src/loop/loop.engine.ts b/apps/server/src/loop/loop.engine.ts index 94e38da..d42ff60 100644 --- a/apps/server/src/loop/loop.engine.ts +++ b/apps/server/src/loop/loop.engine.ts @@ -66,6 +66,16 @@ export const createLoopJob = async ( updatedAt: now, }; const db = await readDb(); + const active = db.loopJobs.filter((j) => j.torrentHash === job.torrentHash && j.status !== "COMPLETED" && j.status !== "STOPPED"); + for (const existing of active) { + existing.status = "STOPPED"; + existing.nextRunAt = undefined; + existing.currentRun = undefined; + existing.updatedAt = nowIso(); + } + if (active.length) { + await writeDb(db); + } db.loopJobs.push(job); await writeDb(db); await logJob(job.id, "INFO", `Loop job started for ${job.name}`, "JOB_STARTED"); diff --git a/apps/server/src/loop/loop.routes.ts b/apps/server/src/loop/loop.routes.ts index 80abb8c..7561dd3 100644 --- a/apps/server/src/loop/loop.routes.ts +++ b/apps/server/src/loop/loop.routes.ts @@ -91,18 +91,24 @@ router.post("/stop-by-hash", async (req, res) => { return res.status(400).json({ error: "Missing hash" }); } const db = await readDb(); - const job = db.loopJobs.find((j) => j.torrentHash === hash); - if (!job) { + const jobs = db.loopJobs.filter((j) => j.torrentHash === hash); + if (!jobs.length) { return res.status(404).json({ error: "Job not found" }); } - const stopped = await stopLoopJob(job.id); + const stopped = [] as any[]; + for (const job of jobs) { + const result = await stopLoopJob(job.id); + if (result) { + stopped.push(result); + } + } try { const qbit = getQbitClient(); await qbit.deleteTorrent(hash, true); } catch (error) { // Best-effort delete } - res.json(stopped); + res.json({ ok: true, stopped }); }); router.post("/dry-run", async (req, res) => { diff --git a/apps/server/src/utils/validators.ts b/apps/server/src/utils/validators.ts index 824b19f..2afda6f 100644 --- a/apps/server/src/utils/validators.ts +++ b/apps/server/src/utils/validators.ts @@ -10,6 +10,8 @@ export const loopStartSchema = z.object({ allowIp: allowIpSchema, targetLoops: z.number().int().min(1).max(1000), delayMs: z.number().int().min(0).max(86_400_000), + profileName: z.string().trim().min(1).max(64).optional(), + profileId: z.string().trim().min(1).optional(), }); export const dryRunSchema = z.object({ diff --git a/apps/web/src/components/loop/LoopSetupCard.tsx b/apps/web/src/components/loop/LoopSetupCard.tsx index 63b606d..7964bbd 100644 --- a/apps/web/src/components/loop/LoopSetupCard.tsx +++ b/apps/web/src/components/loop/LoopSetupCard.tsx @@ -112,6 +112,26 @@ export const LoopSetupCard = () => { } }; + const stopLoop = async () => { + if (!selectedHash) { + return; + } + try { + await api.post("/api/loop/stop-by-hash", { hash: selectedHash }); + pushAlert({ + title: "Loop durduruldu", + description: "Seçili torrent için loop durduruldu.", + variant: "success", + }); + } catch (error) { + pushAlert({ + title: "Durdurma başarısız", + description: "Loop durdurulamadı.", + variant: "error", + }); + } + }; + const applyProfile = async (profile: Profile) => { if (!selectedHash) { pushAlert({ diff --git a/apps/web/src/components/torrents/TorrentTable.tsx b/apps/web/src/components/torrents/TorrentTable.tsx index 64434ae..2e6ea41 100644 --- a/apps/web/src/components/torrents/TorrentTable.tsx +++ b/apps/web/src/components/torrents/TorrentTable.tsx @@ -75,23 +75,36 @@ export const TorrentTable = () => { }; const getProfileName = (hash: string) => { - const job = jobs.find((j) => j.torrentHash === hash); - if (!job) return null; - if (job.profileId) { - const profileById = profiles.find((p) => p.id === job.profileId); + const hashKey = hash.toLowerCase(); + const related = jobs.filter((j) => j.torrentHash?.toLowerCase() === hashKey); + if (related.length === 0) return null; + const active = related.find((j) => j.status === "RUNNING" || j.status === "WAITING_DELAY"); + const latest = related.reduce((current, next) => { + const currentAt = Date.parse(current.updatedAt ?? current.createdAt ?? ""); + const nextAt = Date.parse(next.updatedAt ?? next.createdAt ?? ""); + if (Number.isFinite(nextAt) && Number.isFinite(currentAt)) { + return nextAt >= currentAt ? next : current; + } + if (Number.isFinite(nextAt)) return next; + return current; + }, related[0]); + const candidate = active ?? latest; + if (candidate.profileId) { + const profileById = profiles.find((p) => p.id === candidate.profileId); if (profileById?.name) { return profileById.name; } } - if (job.profileName) { - return job.profileName; + if (candidate.profileName) { + return candidate.profileName; } - const profile = profiles.find((p) => - p.allowIp === job.allowIp && - p.delayMs === job.delayMs && - p.targetLoops === job.targetLoops + const fallback = profiles.find( + (profile) => + profile.allowIp === candidate.allowIp && + profile.delayMs === candidate.delayMs && + profile.targetLoops === candidate.targetLoops ); - return profile?.name ?? null; + return fallback?.name ?? null; }; const filtered = useMemo(() => { diff --git a/apps/web/src/store/useAppStore.ts b/apps/web/src/store/useAppStore.ts index b638d24..bc463bf 100644 --- a/apps/web/src/store/useAppStore.ts +++ b/apps/web/src/store/useAppStore.ts @@ -32,6 +32,8 @@ export interface LoopJob { nextRunAt?: string; lastError?: string; totals?: { totalDownloadedBytes: number }; + createdAt?: string; + updatedAt?: string; } export interface StatusSnapshot {