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:
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user