feat: interaktif panel kontrolleri (Faz 3)

Yayın durdurma, kanal duraklat/devam (tekli+toplu), şimdi kontrol et,
manuel URL ile zorla kayıt başlatma, canlı player+log önizleme, oturum
süre sayacı, başarısız/takılı render için tekrar dene ve iptal, poll
aralığı + ham segment TTL'yi panelden canlı değiştirme, cookie bayatlık
uyarısı, Telegram bildirim aç/kapa, ve kullanım/maliyet özeti (/stats).

api-daemon'ın yeni state-değiştiren endpoint'leri (stop/force-start/
render/cancel) INTERNAL_API_TOKEN ile korunuyor — bu daemon'ın portu
Coolify'de public'e açık olduğu için korumasız bırakmak güvenlik açığı
olurdu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 13:38:47 +03:00
co-authored by Claude Sonnet 5
parent 2fc3254d64
commit 43ccbd9ec7
23 changed files with 742 additions and 75 deletions
@@ -0,0 +1,24 @@
"use client";
export function ConfirmButton({
confirmText,
label,
variant = "err",
}: {
confirmText: string;
label: string;
variant?: "err" | "warn" | "muted" | "ok";
}) {
return (
<button
type="submit"
className={`badge ${variant}`}
style={{ border: "none", cursor: "pointer" }}
onClick={(e) => {
if (!confirm(confirmText)) e.preventDefault();
}}
>
{label}
</button>
);
}
@@ -0,0 +1,53 @@
"use client";
import { useEffect, useRef, useState } from "react";
export function LiveLogViewer({ sessionId }: { sessionId: string }) {
const [lines, setLines] = useState<string[]>([]);
const boxRef = useRef<HTMLPreElement>(null);
useEffect(() => {
let cancelled = false;
async function poll() {
try {
const res = await fetch(`/api/debug/capture/${sessionId}`, { cache: "no-store" });
const data = (await res.json()) as { lines: string[] };
if (!cancelled) setLines(data.lines);
} catch {
// transient fetch failures are fine to just skip — next tick retries
}
}
poll();
const timer = setInterval(poll, 4000);
return () => {
cancelled = true;
clearInterval(timer);
};
}, [sessionId]);
useEffect(() => {
if (boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight;
}, [lines]);
return (
<pre
ref={boxRef}
className="mono"
style={{
maxHeight: "220px",
overflowY: "auto",
background: "var(--bg)",
border: "1px solid var(--border)",
borderRadius: "6px",
padding: "0.6rem 0.7rem",
fontSize: "0.7rem",
whiteSpace: "pre-wrap",
marginTop: "0.5rem",
}}
>
{lines.length > 0 ? lines.join("\n") : "Log bekleniyor…"}
</pre>
);
}
@@ -0,0 +1,23 @@
"use client";
import { useEffect, useState } from "react";
function formatElapsed(ms: number): string {
const totalSec = Math.max(0, Math.floor(ms / 1000));
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
const pad = (n: number) => String(n).padStart(2, "0");
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
}
export function SessionTimer({ startedAt }: { startedAt: string }) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
return <span className="mono">{formatElapsed(now - new Date(startedAt).getTime())}</span>;
}