feat: segment süresi varsayılanını 15 dk -> 5 dk düşür + dashboard'u kart-grid'e çevir

Global SEGMENT_TIME_SEC varsayılanı 900->300 (docker-compose, .env.example,
env.ts, signal_detection.py fallback'i) — kanal bazlı override zaten
panelden ayarlanabiliyordu, bu sadece varsayılanı değiştiriyor.

Dashboard'daki kanal listesi tablo yerine kart-grid: her kanal kendi
kartında, durum rozetleri üstte belirgin, aksiyonlar altta — önceki
tablo düzeni (özellikle aksiyon sütunundaki sıkışık butonlar) okunması
zor bulunuyordu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 22:55:12 +03:00
co-authored by Claude Sonnet 5
parent eca80959fb
commit 58ef40edf8
5 changed files with 69 additions and 74 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
POLL_INTERVAL_MS=60000
SEGMENT_TIME_SEC=900
SEGMENT_TIME_SEC=300
API_DAEMON_PORT=4001
# Signs the panel's login session cookies (frontend). Generate with:
+1 -1
View File
@@ -4,7 +4,7 @@ export const env = {
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
sharedMediaRoot: process.env.SHARED_MEDIA_ROOT ?? "./shared-media",
pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 60_000),
segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 900),
segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 300),
port: Number(process.env.API_DAEMON_PORT ?? 4001),
forceLiveUrl: process.env.FORCE_LIVE_URL ?? "",
ytdlpPotProviderUrl: process.env.YTDLP_POT_PROVIDER_URL ?? "",
+64 -69
View File
@@ -6,8 +6,8 @@ import { formatTr } from "../lib/formatDate";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
import { cn } from "@/lib/utils";
export const dynamic = "force-dynamic";
@@ -47,9 +47,9 @@ export default async function DashboardPage() {
{channels.length === 0 ? (
<p className="text-sm text-muted-foreground">Henüz kanal eklenmedi.</p>
) : (
<Card>
<CardHeader className="flex-row items-center justify-between">
<CardTitle className="text-sm font-medium text-muted-foreground">{channels.length} kanal</CardTitle>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">{channels.length} kanal</span>
<div className="flex gap-2">
<form action={toggleAllChannels.bind(null, true)}>
<Button type="submit" variant="outline" size="sm">
@@ -62,70 +62,65 @@ export default async function DashboardPage() {
</Button>
</form>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Kanal</TableHead>
<TableHead>Durum</TableHead>
<TableHead>Kayıt</TableHead>
<TableHead>Son Kontrol</TableHead>
<TableHead className="text-right">Aksiyon</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channels.map((c) => {
const recording = c.sessions.length > 0;
const status = statuses.get(c.id);
return (
<TableRow key={c.id}>
<TableCell>
<Link href={`/channels/${c.id}`} className="font-medium hover:text-primary hover:underline">
{c.name}
</Link>{" "}
<span className="font-mono-num text-xs text-muted-foreground">{c.youtubeHandle}</span>
</TableCell>
<TableCell>
<Badge variant={c.isActive ? "ok" : "muted"}>{c.isActive ? "Aktif" : "Pasif"}</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1.5">
<Badge variant={recording ? "live" : "muted"}>{recording ? "🔴 Kayıtta" : "—"}</Badge>
{status?.lastPollError && (
<Badge
variant="err"
title={`${formatTr(status.lastPollError.at)}${status.lastPollError.message}`}
>
hata
</Badge>
)}
</div>
</TableCell>
<TableCell className="font-mono-num text-xs text-muted-foreground">
{c.lastCheckedAt ? formatTr(c.lastCheckedAt) : "—"}
</TableCell>
<TableCell>
<div className="flex justify-end gap-1.5">
<form action={toggleChannelActive.bind(null, c.id, !c.isActive)}>
<Button type="submit" variant="outline" size="sm">
{c.isActive ? "Duraklat" : "Devam Ettir"}
</Button>
</form>
<form action={checkChannelNow.bind(null, c.id)}>
<Button type="submit" variant="ghost" size="sm">
Şimdi Kontrol Et
</Button>
</form>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{channels.map((c) => {
const recording = c.sessions.length > 0;
const status = statuses.get(c.id);
return (
<Card
key={c.id}
className={cn("gap-4 py-5", recording && "border-live/40")}
>
<CardHeader className="gap-1 px-5">
<div className="flex items-start justify-between gap-2">
<Link href={`/channels/${c.id}`} className="font-semibold leading-tight hover:text-primary hover:underline">
{c.name}
</Link>
<Badge variant={c.isActive ? "ok" : "muted"} className="shrink-0">
{c.isActive ? "Aktif" : "Pasif"}
</Badge>
</div>
<span className="font-mono-num text-xs text-muted-foreground">{c.youtubeHandle}</span>
</CardHeader>
<CardContent className="flex flex-col gap-2 px-5">
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant={recording ? "live" : "muted"}>
{recording ? "🔴 Kayıtta" : "Kayıtta değil"}
</Badge>
{status?.lastPollError && (
<Badge
variant="err"
title={`${formatTr(status.lastPollError.at)}${status.lastPollError.message}`}
>
hata
</Badge>
)}
</div>
<span className="font-mono-num text-xs text-muted-foreground">
Son kontrol: {c.lastCheckedAt ? formatTr(c.lastCheckedAt) : "—"}
</span>
</CardContent>
<CardFooter className="gap-2 px-5">
<form action={toggleChannelActive.bind(null, c.id, !c.isActive)}>
<Button type="submit" variant="outline" size="sm">
{c.isActive ? "Duraklat" : "Devam Ettir"}
</Button>
</form>
<form action={checkChannelNow.bind(null, c.id)}>
<Button type="submit" variant="ghost" size="sm">
Şimdi Kontrol Et
</Button>
</form>
</CardFooter>
</Card>
);
})}
</div>
</div>
)}
</div>
);
+1 -1
View File
@@ -17,7 +17,7 @@ WINDOW_SEC = 1.0
PEAK_STD_MULTIPLIER = 1.5
CANDIDATE_PAD_SEC = 45
CHAT_SPIKE_MULTIPLIER = 3
SEGMENT_TIME_SEC = int(os.environ.get("SEGMENT_TIME_SEC", "900"))
SEGMENT_TIME_SEC = int(os.environ.get("SEGMENT_TIME_SEC", "300"))
stt_scoring_queue = Queue(QUEUE_NAMES["STT_SCORING"], {"connection": REDIS_URL})
+2 -2
View File
@@ -33,7 +33,7 @@ services:
DATABASE_URL: postgresql://streamclipper:streamclipper@sc_postgres:5432/streamclipper
SHARED_MEDIA_ROOT: /shared-media
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-60000}
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-900}
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-300}
API_DAEMON_PORT: ${API_DAEMON_PORT:-4001}
FORCE_LIVE_URL: ${FORCE_LIVE_URL:-}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
@@ -61,7 +61,7 @@ services:
REDIS_URL: redis://sc_redis:6379
DATABASE_URL: postgresql://streamclipper:streamclipper@sc_postgres:5432/streamclipper
SHARED_MEDIA_ROOT: /shared-media
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-900}
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-300}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}