Add admin-only service status panel with restart, wired to Docker directly

This commit is contained in:
Myron Blair
2026-07-26 17:53:19 -05:00
parent 19374b694e
commit 4d77a36f99
6 changed files with 137 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
import { useEffect, useState, useCallback } from "react";
import api from "../lib/api";
import { toast } from "sonner";
import { RotateCw } from "lucide-react";
export default function ServiceStatus() {
const [services, setServices] = useState([]);
const [restarting, setRestarting] = useState(null);
const load = useCallback(async () => {
try {
const { data } = await api.get("/services");
setServices(data);
} catch {
setServices([]);
}
}, []);
useEffect(() => {
load();
const t = setInterval(load, 15000);
return () => clearInterval(t);
}, [load]);
const restart = async (key, label) => {
setRestarting(key);
try {
await api.post(`/services/${key}/restart`);
toast.success(`${label} restarting…`);
setTimeout(load, 4000);
} catch (err) {
toast.error(err.response?.data?.detail || `Could not restart ${label}`);
} finally {
setRestarting(null);
}
};
if (!services.length) return null;
return (
<div className="px-6 md:px-12 pt-6" data-testid="service-status">
<div className="flex flex-wrap gap-2">
{services.map((s) => (
<div
key={s.key}
className="flex items-center gap-2 bg-[#0F0F0F] border border-[#222] px-3 py-1.5 text-xs"
data-testid={`service-${s.key}`}
>
<span
className={`inline-block w-2 h-2 rounded-full ${s.running ? "bg-[#86efac]" : "bg-[#fca5a5]"}`}
/>
<span className="text-[#8A8A8A]">{s.label}</span>
{!s.running && (
<button
onClick={() => restart(s.key, s.label)}
disabled={restarting === s.key}
className="ml-1 flex items-center gap-1 text-[#D9381E] hover:text-[#ED4B32] disabled:opacity-50"
data-testid={`service-restart-${s.key}`}
>
<RotateCw size={12} className={restarting === s.key ? "animate-spin" : ""} />
Restart
</button>
)}
</div>
))}
</div>
</div>
);
}
+4
View File
@@ -1,12 +1,15 @@
import { useEffect, useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import api from "../lib/api";
import { useAuth } from "../lib/auth";
import Hero from "../components/Hero";
import Row from "../components/Row";
import MovieDetailModal from "../components/MovieDetailModal";
import ServiceStatus from "../components/ServiceStatus";
export default function Browse() {
const nav = useNavigate();
const { user } = useAuth();
const [featured, setFeatured] = useState(null);
const [movies, setMovies] = useState([]);
const [genres, setGenres] = useState([]);
@@ -57,6 +60,7 @@ export default function Browse() {
)}
<div className="-mt-20 relative z-10 pb-24">
{user?.is_admin && <ServiceStatus />}
{continueWatching.length > 0 && (
<Row title="Resume" movies={continueWatching} onCardClick={handleMore} progressMap={progressMap} />
)}