feat(loop): loop logları kalıcı hale getir
Loop loglarının dosya sistemine kaydedilmesi, okunması ve arşivlenmesi için yeni storage modülü eklendi. Loglar artık oturumlar arasında korunur. UI tarafında mobil menü iyileştirmeleri ve log paneli güncellemeleri yapıldı.
This commit is contained in:
@@ -8,7 +8,7 @@ import { useAppStore } from "../../store/useAppStore";
|
||||
import { connectSocket } from "../../socket/socket";
|
||||
import { api } from "../../api/client";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMoon, faSun, faRightFromBracket } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBars, faMoon, faRightFromBracket, faSun } from "@fortawesome/free-solid-svg-icons";
|
||||
import { AlertToastStack } from "../ui/AlertToastStack";
|
||||
|
||||
export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -67,7 +67,17 @@ export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
<header className="rounded-xl border border-slate-200 bg-white/80 px-4 py-3">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-slate-900">q-buffer</div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="text-lg font-semibold text-slate-900">q-buffer</div>
|
||||
<button
|
||||
className={`inline-flex items-center justify-center rounded-md border border-slate-300 px-2 py-2 text-xs font-semibold text-slate-700 md:hidden transition-opacity ${menuOpen ? "pointer-events-none opacity-0" : "opacity-100"}`}
|
||||
onClick={() => setMenuOpen(true)}
|
||||
type="button"
|
||||
aria-label="Menü"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBars} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
qBittorrent {qbit.version ?? "unknown"}
|
||||
</div>
|
||||
@@ -80,18 +90,7 @@ export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded-md border border-slate-300 px-3 py-2 text-xs font-semibold text-slate-700 md:hidden"
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
type="button"
|
||||
>
|
||||
{menuOpen ? "Close" : "Menu"}
|
||||
</button>
|
||||
<div
|
||||
className={`flex flex-col gap-3 md:flex md:flex-row md:items-center ${
|
||||
menuOpen ? "flex" : "hidden md:flex"
|
||||
}`}
|
||||
>
|
||||
<div className="hidden md:flex md:flex-row md:items-center gap-3">
|
||||
<nav className="flex flex-wrap items-center gap-2 rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">
|
||||
<NavLink
|
||||
to="/buffer"
|
||||
@@ -130,6 +129,68 @@ export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
</header>
|
||||
<AlertToastStack />
|
||||
<div
|
||||
className={`fixed inset-0 z-50 md:hidden transition-opacity duration-500 ${menuOpen ? "opacity-100" : "pointer-events-none opacity-0"}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-900/30"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-0 top-0 flex h-full w-64 flex-col gap-6 bg-white p-5 shadow-xl transition-transform duration-500 ease-out ${menuOpen ? "translate-x-0" : "translate-x-full"}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="text-sm font-semibold text-slate-700">Menü</div>
|
||||
<nav className="flex flex-col gap-2 text-sm font-semibold text-slate-700">
|
||||
<NavLink
|
||||
to="/buffer"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
`rounded-lg border px-3 py-2 ${
|
||||
isActive ? "border-slate-900 bg-slate-900 text-white" : "border-slate-200 bg-white"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Buffer
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/timer"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
`rounded-lg border px-3 py-2 ${
|
||||
isActive ? "border-slate-900 bg-slate-900 text-white" : "border-slate-200 bg-white"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Timer
|
||||
</NavLink>
|
||||
</nav>
|
||||
<div className="mt-auto flex items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
applyTheme(theme === "dark" ? "light" : "dark");
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
title={theme === "dark" ? "Light" : "Dark"}
|
||||
className="h-10 w-10 px-0"
|
||||
>
|
||||
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
logout();
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
title="Logout"
|
||||
className="h-10 w-10 px-0"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRightFromBracket} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useAppStore } from "../../store/useAppStore";
|
||||
import { api } from "../../api/client";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card";
|
||||
import { Badge } from "../ui/Badge";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -12,10 +13,27 @@ import {
|
||||
|
||||
export const LogsPanel = () => {
|
||||
const logs = useAppStore((s) => s.logs);
|
||||
const setLogs = useAppStore((s) => s.setLogs);
|
||||
const selectedHash = useAppStore((s) => s.selectedHash);
|
||||
const jobs = useAppStore((s) => s.jobs);
|
||||
const job = jobs.find((j) => j.torrentHash === selectedHash);
|
||||
const filtered = job ? logs.filter((log) => log.jobId === job.id) : logs;
|
||||
useEffect(() => {
|
||||
const loadLogs = async () => {
|
||||
if (!job) {
|
||||
setLogs([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await api.get(`/api/loop/logs/${job.id}`);
|
||||
setLogs(response.data.logs ?? []);
|
||||
} catch (error) {
|
||||
setLogs([]);
|
||||
}
|
||||
};
|
||||
loadLogs();
|
||||
}, [job?.id, setLogs]);
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
@@ -79,12 +79,12 @@ export const TorrentDetailsCard = () => {
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faHashtag} className="text-slate-400" />
|
||||
<span className="min-w-0 truncate">Hash: {torrent.hash}</span>
|
||||
<span className="min-w-0 truncate"> Hash: {torrent.hash}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faDatabase} className="text-slate-400" />
|
||||
<span>
|
||||
Size: {(torrent.size / (1024 * 1024 * 1024)).toFixed(2)} GB
|
||||
Size: {(torrent.size / (1024 * 1024 * 1024)).toFixed(2)} GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 truncate" title={torrent.tracker || "-"}>
|
||||
|
||||
@@ -169,7 +169,7 @@ export const TorrentTable = () => {
|
||||
{torrent.name}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1 text-xs">
|
||||
<div className="h-2 w-28 rounded-full bg-slate-200">
|
||||
<div className="h-2 w-28 shrink-0 rounded-full bg-slate-200">
|
||||
<div
|
||||
className="h-2 rounded-full bg-mint"
|
||||
style={{ width: `${Math.round(torrent.progress * 100)}%` }}
|
||||
@@ -177,7 +177,7 @@ export const TorrentTable = () => {
|
||||
</div>
|
||||
<span className="w-8 text-left tabular-nums">{Math.round(torrent.progress * 100)}%</span>
|
||||
<span className="w-16 text-left tabular-nums">{formatSpeed(torrent.dlspeed)}</span>
|
||||
<span className="flex items-center gap-2 text-slate-500 group-[.is-selected]:text-white">
|
||||
<span className="flex items-center gap-2 text-slate-500 group-[.is-selected]:text-white whitespace-nowrap">
|
||||
{renderState(torrent.state)}
|
||||
{getProfileName(torrent.hash) && (
|
||||
<span className="text-xs text-slate-500 group-[.is-selected]:text-white">{getProfileName(torrent.hash)}</span>
|
||||
|
||||
@@ -86,6 +86,7 @@ interface AppState {
|
||||
setSnapshot: (snapshot: StatusSnapshot) => void;
|
||||
updateStatus: (snapshot: Partial<StatusSnapshot>) => void;
|
||||
addLog: (log: JobLog) => void;
|
||||
setLogs: (logs: JobLog[]) => void;
|
||||
setTimerRules: (rules: TimerRule[]) => void;
|
||||
setTimerLogs: (logs: TimerLog[]) => void;
|
||||
addTimerLog: (log: TimerLog) => void;
|
||||
@@ -125,6 +126,7 @@ export const useAppStore = create<AppState>((set) => ({
|
||||
set((state) => ({
|
||||
logs: [...state.logs, log].slice(-500),
|
||||
})),
|
||||
setLogs: (logs) => set({ logs }),
|
||||
setTimerRules: (rules) => set({ timerRules: rules }),
|
||||
setTimerLogs: (logs) => set({ timerLogs: logs }),
|
||||
addTimerLog: (log) =>
|
||||
|
||||
Reference in New Issue
Block a user