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 {