Refatora infraestrutura de dados, build e provisionamento de streams
---
- Movido streams.json para /app/data/streams/streams.json; volume do compose mapeado especificamente em /app/data/streams, deixando logs fora do volume persistido;
- Adicionado scripts/reprovision.mjs que regenera stream.conf e tokens VNC a partir dos templates da imagem no startup, garantindo que updates de container não exijam recriar o volume;
- Removido autologin.template.sh por-stream; substituído por scripts/autologin.sh global na imagem, com variáveis passadas via environment= no supervisor conf (com valores entre aspas
para compatibilidade com valores vazios);
- Logs de processos por stream movidos de /app/data/streams/{id}/ para /app/data/logs/{id}/;
- Adicionada função recreateStream em supervisor.ts e rota POST /api/streams/[id]/recreate; botão "Recreate" adicionado ao menu do card para limpar chrome-profile e re-provisionar;
- Adicionado auto-disparo de captureThumb no GET /api/streams/[id]/thumb quando thumb.jpg não existe e nenhuma captura está em andamento;
- Dockerfile: adicionado --mount=type=cache para /var/cache/apt e /var/lib/apt/lists (não /var/lib/apt inteiro para evitar corrupção de estado); removido --no-cache do Makefile; remoção
de pacotes limitada a curl gnupg para evitar cascata em dependências do chromium/novnc;
- Migração automática de streams.json do caminho antigo adicionada ao entrypoint.sh;
---
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getStream } from "@/lib/db"
|
||||
import { recreateStream } from "@/lib/supervisor"
|
||||
|
||||
type Ctx = { params: Promise<{ id: string }> }
|
||||
|
||||
export async function POST(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
if (!getStream(id)) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
recreateStream(id)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
@@ -11,11 +11,21 @@ type Ctx = { params: Promise<{ id: string }> }
|
||||
export async function GET(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
const thumbPath = path.join(DATA_DIR, "streams", id, "thumb.jpg")
|
||||
if (!fs.existsSync(thumbPath)) return new Response("not found", { status: 404 })
|
||||
const buffer = fs.readFileSync(thumbPath)
|
||||
return new Response(buffer, {
|
||||
headers: { "Content-Type": "image/jpeg", "Cache-Control": "no-cache, no-store" },
|
||||
})
|
||||
const tmpPath = path.join(DATA_DIR, "streams", id, "thumb.tmp.jpg")
|
||||
|
||||
if (fs.existsSync(thumbPath)) {
|
||||
const buffer = fs.readFileSync(thumbPath)
|
||||
return new Response(buffer, {
|
||||
headers: { "Content-Type": "image/jpeg", "Cache-Control": "no-cache, no-store" },
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-trigger capture when no thumb exists and no capture is already in progress
|
||||
if (!fs.existsSync(tmpPath) && getStream(id)) {
|
||||
captureThumb(id, 5)
|
||||
}
|
||||
|
||||
return new Response("not found", { status: 404 })
|
||||
}
|
||||
|
||||
export async function POST(_req: Request, { params }: Ctx) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { MoreHorizontal, Play, Globe, Monitor, Pencil, RotateCcw, Square, Trash2, Circle, Copy, Check, Video, ImageUp, GripVertical } from "lucide-react"
|
||||
import { MoreHorizontal, Play, Globe, Monitor, Pencil, RotateCcw, Square, Trash2, Circle, Copy, Check, Video, ImageUp, GripVertical, Wrench } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { Stream } from "@/types/stream"
|
||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||
@@ -230,6 +230,9 @@ export function StreamCard({ stream, status, localStatus, cardSize = "md", onRef
|
||||
<button onClick={() => action("restart", "restarting")} className={menuItem}>
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Restart
|
||||
</button>
|
||||
<button onClick={() => action("recreate", "restarting")} className={menuItem}>
|
||||
<Wrench className="w-3.5 h-3.5" /> Recreate
|
||||
</button>
|
||||
{status?.ffmpeg === "RUNNING" || localStatus === "restarting" ? (
|
||||
<button onClick={() => action("stop", "stopping")} className={menuItem}>
|
||||
<Square className="w-3.5 h-3.5" /> Stop
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import path from "path"
|
||||
import type { Stream } from "@/types/stream"
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR ?? "/app/data"
|
||||
const STREAMS_FILE = path.join(DATA_DIR, "streams.json")
|
||||
const STREAMS_FILE = path.join(DATA_DIR, "streams", "streams.json")
|
||||
|
||||
function ensureFile() {
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })
|
||||
|
||||
+12
-7
@@ -42,6 +42,7 @@ export function normalizeScale(scale: string): string {
|
||||
export function provisionStream(stream: Stream): void {
|
||||
const dir = streamDir(stream.id)
|
||||
fs.mkdirSync(path.join(dir, "chrome-profile"), { recursive: true })
|
||||
fs.mkdirSync(path.join(DATA_DIR, "logs", stream.id), { recursive: true })
|
||||
|
||||
const vars: Record<string, string | number> = {
|
||||
STREAM_ID: stream.id,
|
||||
@@ -64,14 +65,8 @@ export function provisionStream(stream: Stream): void {
|
||||
PASS: stream.pass ?? "",
|
||||
}
|
||||
|
||||
const autologinTpl = fs.readFileSync("/opt/scripts/autologin.template.sh", "utf-8")
|
||||
const autologinPath = path.join(dir, "autologin.sh")
|
||||
fs.writeFileSync(autologinPath, render(autologinTpl, vars), "utf-8")
|
||||
fs.chmodSync(autologinPath, 0o755)
|
||||
|
||||
const confTpl = fs.readFileSync("/opt/scripts/stream.template.conf", "utf-8")
|
||||
const confPath = path.join(dir, "stream.conf")
|
||||
fs.writeFileSync(confPath, render(confTpl, vars), "utf-8")
|
||||
fs.writeFileSync(path.join(dir, "stream.conf"), render(confTpl, vars), "utf-8")
|
||||
|
||||
fs.mkdirSync(VNC_TOKENS_DIR, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
@@ -84,6 +79,16 @@ export function provisionStream(stream: Stream): void {
|
||||
supervisorctl("update")
|
||||
}
|
||||
|
||||
export function recreateStream(id: string): void {
|
||||
const stream = getStream(id)
|
||||
if (!stream) return
|
||||
stopStream(id)
|
||||
const dir = streamDir(id)
|
||||
fs.rmSync(path.join(dir, "chrome-profile"), { recursive: true, force: true })
|
||||
provisionStream(stream)
|
||||
startStream(id)
|
||||
}
|
||||
|
||||
export function startStream(id: string): void {
|
||||
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
||||
for (const p of programs) supervisorctl(`start ${p}-${id}`)
|
||||
|
||||
Reference in New Issue
Block a user