feat(web): add Ops page and proxy control on :8600

Vite listens on 8500 and proxies /ops to the control plane. Admin Ops UI
covers pause/resume, scale, drain/force restart, and restart-needed banner.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-19 10:55:02 +00:00
co-authored by nsrb
parent 756ba8a7ac
commit 4e6c336ae8
21 changed files with 2931 additions and 269 deletions
+82 -1
View File
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "./client.js";
import { api, opsApi } from "./client.js";
export function useBootstrap() {
return useQuery({
@@ -450,3 +450,84 @@ export function useDeleteHttpAuth() {
export async function fetchHttpAuthLiterals(name) {
return (await api.get(`/http-auths/${encodeURIComponent(name)}/reveal`)).data;
}
export function useOpsStatus(enabled = true) {
return useQuery({
queryKey: ["ops-status"],
queryFn: async () => (await opsApi.get("/status")).data,
enabled,
retry: false,
refetchInterval: enabled ? 3000 : false,
});
}
export function useOpsPause() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => (await opsApi.post("/pause")).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}
export function useOpsResume() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => (await opsApi.post("/resume")).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}
export function useOpsReload() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => (await opsApi.post("/reload")).data,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ops-status"] });
qc.invalidateQueries({ queryKey: ["workflows"] });
},
});
}
export function useOpsRestart() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ force = false } = {}) =>
(await opsApi.post("/restart", { force })).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}
export function useOpsScale() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ workers, force = false }) =>
(await opsApi.post("/scale", { workers, force })).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}
export function useOpsHttpStart() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => (await opsApi.post("/http/start")).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}
export function useOpsHttpStop() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => (await opsApi.post("/http/stop")).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}
export function useOpsBumpGeneration() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (reason) =>
(await opsApi.post("/generation/bump", { reason })).data,
onSuccess: () => qc.invalidateQueries({ queryKey: ["ops-status"] }),
});
}