1
0
forked from wrenn/wrenn

Add admin template deletion with broadcast to all hosts

- DELETE /v1/admin/templates/{name} endpoint (admin-only)
- Broadcasts DeleteSnapshot RPC to all online hosts before removing DB record
- Frontend admin templates page uses deleteAdminTemplate() instead of
  team-scoped deleteSnapshot()
- Delete button shown for all template types, not just snapshots
This commit is contained in:
2026-03-26 23:53:08 +06:00
parent c0d6381bbe
commit 5cb37bf2a0
4 changed files with 61 additions and 15 deletions

View File

@ -64,3 +64,7 @@ export type AdminTemplate = {
export async function listAdminTemplates(): Promise<ApiResult<AdminTemplate[]>> {
return apiFetch('GET', '/api/v1/admin/templates');
}
export async function deleteAdminTemplate(name: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/admin/templates/${name}`);
}

View File

@ -3,11 +3,11 @@
import { onMount, onDestroy } from 'svelte';
import { toast } from '$lib/toast.svelte';
import { formatDate, timeAgo } from '$lib/utils/format';
import { deleteSnapshot } from '$lib/api/capsules';
import {
listBuilds,
createBuild,
listAdminTemplates,
deleteAdminTemplate,
type Build,
type BuildLogEntry,
type AdminTemplate
@ -145,7 +145,7 @@
deleting = true;
deleteError = null;
const name = deleteTarget.name;
const result = await deleteSnapshot(name);
const result = await deleteAdminTemplate(name);
if (result.ok) {
templates = templates.filter((t) => t.name !== name);
deleteTarget = null;
@ -413,14 +413,12 @@
</span>
</td>
<td class="px-4 py-3.5 text-right">
{#if tmpl.type === 'snapshot'}
<button
onclick={() => { deleteTarget = tmpl; deleteError = null; }}
class="rounded-[var(--radius-button)] px-3 py-1.5 text-meta text-[var(--color-text-tertiary)] transition-colors duration-150 hover:bg-[var(--color-red)]/10 hover:text-[var(--color-red)]"
>
Delete
</button>
{/if}
</td>
</tr>
{/each}

View File

@ -7,21 +7,25 @@ import (
"net/http"
"time"
"connectrpc.com/connect"
"github.com/go-chi/chi/v5"
"git.omukk.dev/wrenn/sandbox/internal/db"
"git.omukk.dev/wrenn/sandbox/internal/id"
"git.omukk.dev/wrenn/sandbox/internal/lifecycle"
"git.omukk.dev/wrenn/sandbox/internal/service"
"git.omukk.dev/wrenn/sandbox/internal/validate"
pb "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen"
)
type buildHandler struct {
svc *service.BuildService
db *db.Queries
pool *lifecycle.HostClientPool
}
func newBuildHandler(svc *service.BuildService, db *db.Queries) *buildHandler {
return &buildHandler{svc: svc, db: db}
func newBuildHandler(svc *service.BuildService, db *db.Queries, pool *lifecycle.HostClientPool) *buildHandler {
return &buildHandler{svc: svc, db: db, pool: pool}
}
type createBuildRequest struct {
@ -202,3 +206,42 @@ func (h *buildHandler) ListTemplates(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, resp)
}
// DeleteTemplate handles DELETE /v1/admin/templates/{name}.
func (h *buildHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
if err := validate.SafeName(name); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", fmt.Sprintf("invalid template name: %s", err))
return
}
ctx := r.Context()
if _, err := h.db.GetTemplate(ctx, name); err != nil {
writeError(w, http.StatusNotFound, "not_found", "template not found")
return
}
// Broadcast delete to all online hosts.
hosts, _ := h.db.ListActiveHosts(ctx)
for _, host := range hosts {
if host.Status != "online" {
continue
}
agent, err := h.pool.GetForHost(host)
if err != nil {
continue
}
if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{Name: name})); err != nil {
if connect.CodeOf(err) != connect.CodeNotFound {
slog.Warn("admin: failed to delete template on host", "host_id", id.FormatHostID(host.ID), "name", name, "error", err)
}
}
}
if err := h.db.DeleteTemplate(ctx, name); err != nil {
writeError(w, http.StatusInternalServerError, "db_error", "failed to delete template record")
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -67,7 +67,7 @@ func New(
auditH := newAuditHandler(auditSvc)
statsH := newStatsHandler(statsSvc)
metricsH := newSandboxMetricsHandler(queries, pool)
buildH := newBuildHandler(buildSvc, queries)
buildH := newBuildHandler(buildSvc, queries, pool)
// OpenAPI spec and docs.
r.Get("/openapi.yaml", serveOpenAPI)
@ -178,6 +178,7 @@ func New(
r.Use(requireAdmin(queries))
r.Put("/teams/{id}/byoc", teamH.SetBYOC)
r.Get("/templates", buildH.ListTemplates)
r.Delete("/templates/{name}", buildH.DeleteTemplate)
r.Post("/builds", buildH.Create)
r.Get("/builds", buildH.List)
r.Get("/builds/{id}", buildH.Get)