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