fix: kanal detay sayfasında iç içe girinti (segment/aday klip) netleştirildi
3 seviyeli border-left girinti (oturum > segment > aday klip) yerine
gerçek kutu (soft-card) blokları — her segment kendi arka planıyla
ayrışıyor, adaylar onun içinde daha da net bir alt kutuda. Segment
başlığı artık ham dosya yolu değil ("15sn segment · 02.09.2026 20:14"
gibi) insan-okur bir etiket — dosya yolu küçük/soluk referans olarak
altta kaldı. "Canlı Durum" kartındaki karışık bölümler de (hata/canlı
önizleme/log/segment ayarı/manuel başlat) Separator ile ayrıştırıldı.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge, type badgeVariants } from "@/components/ui/badge";
|
import { Badge, type badgeVariants } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import type { VariantProps } from "class-variance-authority";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -111,12 +112,51 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<p className="font-mono-num text-xs text-muted-foreground">
|
<p className="font-mono-num text-xs text-muted-foreground">
|
||||||
Aktif: {channel.isActive ? "evet" : "hayır"} · Son kontrol:{" "}
|
Aktif: {channel.isActive ? "evet" : "hayır"} · Son kontrol:{" "}
|
||||||
{channel.lastCheckedAt ? formatTr(channel.lastCheckedAt) : "—"}
|
{channel.lastCheckedAt ? formatTr(channel.lastCheckedAt) : "—"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{status === undefined && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
api-daemon'dan canlı durum alınamadı (servis erişilemez olabilir).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status?.lastPollError && (
|
||||||
|
<div className="flex flex-col gap-1.5 rounded-lg border border-err/30 bg-err/5 p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="err" className="w-fit">
|
||||||
|
son poll hatası
|
||||||
|
</Badge>
|
||||||
|
<span className="font-mono-num text-xs text-muted-foreground">
|
||||||
|
{formatTr(status.lastPollError.at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<pre className="font-mono-num whitespace-pre-wrap text-[0.7rem] text-muted-foreground">
|
||||||
|
{status.lastPollError.message}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status?.recording && (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{activeSession?.liveVideoId && activeSession.liveVideoId !== "forced" && (
|
||||||
|
<iframe
|
||||||
|
src={`https://www.youtube.com/embed/${activeSession.liveVideoId}`}
|
||||||
|
title="Canlı yayın önizleme"
|
||||||
|
className="aspect-video w-full max-w-md rounded-md border border-border"
|
||||||
|
allow="autoplay; encrypted-media"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeSession && <LiveLogViewer sessionId={activeSession.id} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<form
|
<form
|
||||||
action={updateChannelSegmentTime.bind(null, channel.id)}
|
action={updateChannelSegmentTime.bind(null, channel.id)}
|
||||||
className="flex flex-wrap items-end gap-2"
|
className="flex flex-wrap items-end gap-2"
|
||||||
@@ -132,61 +172,26 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
min={1}
|
min={1}
|
||||||
max={60}
|
max={60}
|
||||||
step={0.5}
|
step={0.5}
|
||||||
placeholder="varsayılan (15)"
|
placeholder="varsayılan"
|
||||||
defaultValue={channel.segmentTimeSec ? channel.segmentTimeSec / 60 : ""}
|
defaultValue={channel.segmentTimeSec ? channel.segmentTimeSec / 60 : ""}
|
||||||
className="w-40"
|
className="w-32"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="outline" size="sm">
|
<Button type="submit" variant="outline" size="sm">
|
||||||
Kaydet
|
Kaydet
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Boş bırakılırsa global varsayılan kullanılır. Sadece bundan sonra başlayacak kayıtları etkiler.
|
|
||||||
</span>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{status === undefined && (
|
<details className="text-sm">
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
api-daemon'dan canlı durum alınamadı (servis erişilemez olabilir).
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status?.lastPollError && (
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="err" className="w-fit">
|
|
||||||
son poll hatası
|
|
||||||
</Badge>
|
|
||||||
<span className="font-mono-num text-xs text-muted-foreground">
|
|
||||||
{formatTr(status.lastPollError.at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<pre className="font-mono-num whitespace-pre-wrap rounded-md border border-border bg-background/60 p-3 text-[0.7rem] text-muted-foreground">
|
|
||||||
{status.lastPollError.message}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status?.recording && activeSession?.liveVideoId && activeSession.liveVideoId !== "forced" && (
|
|
||||||
<iframe
|
|
||||||
src={`https://www.youtube.com/embed/${activeSession.liveVideoId}`}
|
|
||||||
title="Canlı yayın önizleme"
|
|
||||||
className="aspect-video w-full max-w-md rounded-md border border-border"
|
|
||||||
allow="autoplay; encrypted-media"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status?.recording && activeSession && <LiveLogViewer sessionId={activeSession.id} />}
|
|
||||||
|
|
||||||
<details className="mt-1 text-sm">
|
|
||||||
<summary className="cursor-pointer font-mono-num text-xs text-muted-foreground">
|
<summary className="cursor-pointer font-mono-num text-xs text-muted-foreground">
|
||||||
Manuel URL ile kayıt başlat
|
Manuel URL ile kayıt başlat
|
||||||
</summary>
|
</summary>
|
||||||
<form action={forceStartCapture.bind(null, channel.id)} className="mt-2 flex gap-2">
|
<form action={forceStartCapture.bind(null, channel.id)} className="mt-2 flex gap-2">
|
||||||
<Input name="url" placeholder="https://www.youtube.com/watch?v=..." required className="flex-1" />
|
<Input name="url" placeholder="https://www.youtube.com/watch?v=..." required className="w-64" />
|
||||||
<Button type="submit">Başlat</Button>
|
<Button type="submit">Başlat</Button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -213,11 +218,14 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
) : (
|
) : (
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{session.segments.map((segment) => (
|
{session.segments.map((segment) => (
|
||||||
<div key={segment.id} className="border-l-2 border-border pl-4">
|
<div key={segment.id} className="rounded-lg border border-border bg-muted/20 p-4">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="font-mono-num text-xs text-muted-foreground">{segment.filePath}</span>
|
<span className="text-sm font-medium">
|
||||||
|
{segment.duration}sn segment · {formatTr(segment.startedAt)}
|
||||||
|
</span>
|
||||||
<Badge variant={STATUS_BADGE[segment.status] ?? "muted"}>{segment.status}</Badge>
|
<Badge variant={STATUS_BADGE[segment.status] ?? "muted"}>{segment.status}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="font-mono-num text-[0.7rem] text-muted-foreground">{segment.filePath}</span>
|
||||||
|
|
||||||
<video controls preload="metadata" className="mt-2 w-full max-w-md rounded-md border border-border">
|
<video controls preload="metadata" className="mt-2 w-full max-w-md rounded-md border border-border">
|
||||||
<source src={`/api/segments/${segment.id}/video`} type="video/mp4" />
|
<source src={`/api/segments/${segment.id}/video`} type="video/mp4" />
|
||||||
@@ -236,13 +244,15 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{segment.candidates.length === 0 && segment.status !== "PENDING" && (
|
{segment.candidates.length === 0 && segment.status !== "PENDING" && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-3 text-xs text-muted-foreground">
|
||||||
Bu segmentte aday klip bulunamadı (sinyal analizi ilginç bir an tespit etmedi).
|
Bu segmentte aday klip bulunamadı (sinyal analizi ilginç bir an tespit etmedi).
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{segment.candidates.length > 0 && (
|
||||||
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{segment.candidates.map((c) => (
|
{segment.candidates.map((c) => (
|
||||||
<div key={c.id} className="mt-3 border-l-2 border-border pl-4">
|
<div key={c.id} className="rounded-md border border-border bg-background/60 p-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="font-mono-num text-xs text-muted-foreground">
|
<span className="font-mono-num text-xs text-muted-foreground">
|
||||||
{c.startSec}s – {c.endSec}s
|
{c.startSec}s – {c.endSec}s
|
||||||
@@ -304,6 +314,8 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user