Phase 3: env masking, image updates, port conflicts, resources, templates (0.3.0)

Backend:
- update_service: registry manifest digest check (Docker Hub/ghcr/lscr/private
  v2 token auth) vs local RepoDigests; in-memory cache + background loop
- port_service: parse compose ports, check /proc/net/tcp[6] + docker bindings
- template_service + bundled templates (jellyfin/vaultwarden/uptime-kuma/
  paperless-ngx/gitea) with {{VAR}} placeholders; custom templates in DB
- compose_edit set_resources (deploy.resources.limits/reservations)
- routers: images, ports, templates, editor/set-resources
- Template model; background update task wired into lifespan

Frontend:
- EnvEditor (table + raw, sensitive masking, quick-insert)
- Images page + UpdateBadge + dashboard 'updates available' banner
- PortConflictDialog pre-deploy check on Deploy
- ResourcePanel (CPU/RAM sliders) as editor Limits tab
- Templates page with per-variable instantiate form

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-07 16:49:38 +00:00
co-authored by Claude Opus 4.8
parent b553c1b861
commit 22d9864436
32 changed files with 1728 additions and 21 deletions
+13
View File
@@ -30,4 +30,17 @@ export const editorApi = {
value,
})
.then((r) => r.data.yaml),
setResources: (
yaml: string,
service: string,
res: {
cpus?: number | null;
memory?: string | null;
cpus_reserve?: number | null;
memory_reserve?: string | null;
}
) =>
api
.post<{ yaml: string }>("/api/editor/set-resources", { yaml, service, ...res })
.then((r) => r.data.yaml),
};
+27
View File
@@ -0,0 +1,27 @@
import api from "./client";
export interface UpdateStatus {
image: string;
update_available: boolean;
current_digest: string | null;
remote_digest: string | null;
checked_at: number;
error: string | null;
}
export interface ImageRow {
id: string;
tag: string;
size: number;
created: string;
stacks: string[];
update: UpdateStatus | null;
}
export const imagesApi = {
list: () => api.get<ImageRow[]>("/api/images").then((r) => r.data),
updates: () =>
api.get<Record<string, UpdateStatus>>("/api/images/updates").then((r) => r.data),
check: () =>
api.post<Record<string, UpdateStatus>>("/api/images/check").then((r) => r.data),
};
+18
View File
@@ -0,0 +1,18 @@
import api from "./client";
export interface PortConflict {
port: number;
protocol: string;
service: string | null;
used_by: string;
}
export const portsApi = {
conflicts: (yaml: string, ignore_stack?: string) =>
api
.post<{ conflicts: PortConflict[] }>("/api/ports/conflicts", {
yaml,
ignore_stack,
})
.then((r) => r.data.conflicts),
};
+38
View File
@@ -0,0 +1,38 @@
import api from "./client";
export interface TemplateVariable {
name: string;
description: string;
default: string;
}
export interface TemplateSummary {
id: string;
name: string;
description?: string | null;
tags: string[];
gpu?: string | null;
source: "bundled" | "custom";
}
export interface TemplateDetail extends TemplateSummary {
yaml: string;
variables: TemplateVariable[];
}
export const templatesApi = {
list: () => api.get<TemplateSummary[]>("/api/templates").then((r) => r.data),
get: (id: string) =>
api.get<TemplateDetail>(`/api/templates/${id}`).then((r) => r.data),
instantiate: (id: string, name: string, values: Record<string, string>) =>
api
.post<{ id: string; name: string }>(`/api/templates/${id}/instantiate`, {
name,
values,
})
.then((r) => r.data),
save: (body: { name: string; description?: string; tags: string[]; yaml: string }) =>
api.post("/api/templates", body).then((r) => r.data),
remove: (slug: string) =>
api.delete(`/api/templates/custom/${slug}`).then((r) => r.data),
};