Compare commits

...

2 Commits

Author SHA1 Message Date
967eb2d2a4 fix(server): hata yönetimini ve dayanıklılığı iyileştir
Loop scheduler ve timer worker için hata yakalama ekle. qBit client'ta
geçici ağ hatalarını tanıyarak login durumunu sıfırla. Scheduler
hatalarında durum güncellemesi gönder ve timer worker crash önle.
2026-01-31 11:25:51 +03:00
9b495b7bf7 fix(server): hata yönetimini iyileştir
Zamanlayıcı ve qbit istemcisi bileşenlerinde hata işleme yeteneklerini
güçlendirir.

- loop.scheduler: qbit hatalarında sistem durumunu ve sağlık bilgisini
  güncelleme ekler.
- qbit.client: geçici ağ hatalarını (EAI_AGAIN vb.) algılayarak oturum
  durumunu sıfırlar.
- timer.worker: global hata yakalama ekleyerek işleyicinin çökmesini
  engeller ve hataları günlüğe kaydeder.
2026-01-31 11:04:31 +03:00
8 changed files with 6194 additions and 424 deletions

View File

@@ -1,7 +1,7 @@
import { QbitClient } from "../qbit/qbit.client" import { QbitClient } from "../qbit/qbit.client"
import { tickLoopJobs } from "./loop.engine" import { tickLoopJobs } from "./loop.engine"
import { getStatusSnapshot, refreshJobsStatus, setTorrentsStatus } from "../status/status.service" import { getStatusSnapshot, refreshJobsStatus, setQbitStatus, setTorrentsStatus } from "../status/status.service"
import { emitStatusUpdate } from "../realtime/emitter" import { emitQbitHealth, emitStatusUpdate } from "../realtime/emitter"
import { logger } from "../utils/logger" import { logger } from "../utils/logger"
export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => { export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => {
@@ -20,7 +20,19 @@ export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => {
jobs, jobs,
}); });
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
logger.error({ error }, "Loop scheduler tick failed"); logger.error({ error }, "Loop scheduler tick failed");
setQbitStatus({ ok: false, lastError: message });
emitQbitHealth({ ok: false, lastError: message });
try {
const current = await getStatusSnapshot();
emitStatusUpdate({
...current,
qbit: { ...current.qbit, ok: false, lastError: message },
});
} catch {
// Swallow secondary status errors to keep scheduler alive.
}
} }
}, intervalMs); }, intervalMs);
}; };

View File

@@ -48,6 +48,18 @@ export class QbitClient {
} }
return await fn(); return await fn();
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) {
const code = error.code ?? "";
const transient =
code === "EAI_AGAIN" ||
code === "ENOTFOUND" ||
code === "ECONNREFUSED" ||
code === "ECONNRESET" ||
code === "ETIMEDOUT";
if (transient) {
this.loggedIn = false;
}
}
if ( if (
axios.isAxiosError(error) && axios.isAxiosError(error) &&
(error.response?.status === 401 || error.response?.status === 403) (error.response?.status === 401 || error.response?.status === 403)

View File

@@ -4,6 +4,7 @@ import { readDb, writeDb } from "../storage/jsondb";
import { TimerLog, TimerSummary } from "../types"; import { TimerLog, TimerSummary } from "../types";
import { emitTimerLog, emitTimerSummary } from "../realtime/emitter"; import { emitTimerLog, emitTimerSummary } from "../realtime/emitter";
import { nowIso } from "../utils/time"; import { nowIso } from "../utils/time";
import { logger } from "../utils/logger";
const MAX_LOGS = 2000; const MAX_LOGS = 2000;
@@ -17,6 +18,7 @@ const normalizeTags = (tags?: string, category?: string) => {
export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => { export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => {
setInterval(async () => { setInterval(async () => {
try {
const db = await readDb(); const db = await readDb();
const rules = db.timerRules ?? []; const rules = db.timerRules ?? [];
if (rules.length === 0) { if (rules.length === 0) {
@@ -88,5 +90,8 @@ export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => {
db.timerSummary = summary; db.timerSummary = summary;
await writeDb(db); await writeDb(db);
} }
} catch (error) {
logger.error({ error }, "Timer worker tick failed");
}
}, intervalMs); }, intervalMs);
}; };

View File

@@ -13,6 +13,7 @@
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.2",
"axios": "^1.7.7", "axios": "^1.7.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"react": "^18.3.1", "react": "^18.3.1",

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import clsx from "clsx";
export const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={clsx("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full overflow-y-auto rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Scrollbar
orientation="vertical"
className="flex w-2.5 touch-none select-none rounded-full bg-transparent p-0.5 opacity-100"
>
<ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-slate-200" />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = "ScrollArea";

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/Card"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/Card";
import { Button } from "../components/ui/Button"; import { Button } from "../components/ui/Button";
import { Input } from "../components/ui/Input"; import { Input } from "../components/ui/Input";
import { ScrollArea } from "../components/ui/ScrollArea";
import { api } from "../api/client"; import { api } from "../api/client";
import { useAppStore } from "../store/useAppStore"; import { useAppStore } from "../store/useAppStore";
import { useUiStore } from "../store/useUiStore"; import { useUiStore } from "../store/useUiStore";
@@ -360,13 +361,21 @@ export const TimerPage = () => {
<SelectItem <SelectItem
key={option.value} key={option.value}
value={option.value} value={option.value}
onClick={() => { onPointerDown={() => {
if (option.value === sortKey) { if (option.value === sortKey) {
setSortDirection((current) => setSortDirection((current) =>
current === "asc" ? "desc" : "asc" current === "asc" ? "desc" : "asc"
); );
} }
}} }}
onKeyDown={(event) => {
if (option.value !== sortKey) return;
if (event.key === "Enter" || event.key === " ") {
setSortDirection((current) =>
current === "asc" ? "desc" : "asc"
);
}
}}
> >
{option.label} {option.label}
</SelectItem> </SelectItem>
@@ -435,11 +444,13 @@ export const TimerPage = () => {
Silinen Torrent Logları Silinen Torrent Logları
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3 overflow-hidden">
{timerLogs.length === 0 ? ( {timerLogs.length === 0 ? (
<div className="text-sm text-slate-500">Henüz log yok.</div> <div className="text-sm text-slate-500">Henüz log yok.</div>
) : ( ) : (
<div className="space-y-3"> <div className="overflow-hidden" style={{ height: 560 }}>
<ScrollArea className="h-full w-full" type="always">
<div className="space-y-3 pr-3">
{timerLogs.map((log) => ( {timerLogs.map((log) => (
<div <div
key={log.id} key={log.id}
@@ -469,6 +480,8 @@ export const TimerPage = () => {
</div> </div>
))} ))}
</div> </div>
</ScrollArea>
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>

748
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

5610
pnpm-lock_.yaml Normal file

File diff suppressed because it is too large Load Diff