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
+36 -9
View File
@@ -5,7 +5,10 @@ import Editor from "@monaco-editor/react";
import { Rocket, Save, Wand2, FileCode } from "lucide-react";
import { Button, Card, Input } from "@/components/ui";
import { EditorHelperPanel } from "@/components/stacks/EditorHelperPanel";
import { EnvEditor } from "@/components/env/EnvEditor";
import { PortConflictDialog } from "@/components/stacks/PortConflictDialog";
import { stacksApi } from "@/api/stacks";
import { portsApi, type PortConflict } from "@/api/ports";
import { apiErrorMessage } from "@/api/client";
import { useThemeStore } from "@/store/theme";
import { toast } from "sonner";
@@ -33,6 +36,8 @@ export function StackEditor() {
const [saving, setSaving] = useState(false);
const [convertOpen, setConvertOpen] = useState(false);
const [runCmd, setRunCmd] = useState("");
const [conflicts, setConflicts] = useState<PortConflict[] | null>(null);
const [checking, setChecking] = useState(false);
const existing = useQuery({
queryKey: ["stack", id],
@@ -79,6 +84,22 @@ export function StackEditor() {
}
};
const onDeploy = async () => {
setChecking(true);
try {
const found = await portsApi.conflicts(yaml, id);
if (found.length > 0) {
setConflicts(found);
return;
}
} catch {
/* if the check fails, fall through and let compose surface errors */
} finally {
setChecking(false);
}
save(true);
};
const convert = async () => {
try {
const { yaml: converted } = await stacksApi.convert(runCmd);
@@ -145,14 +166,9 @@ export function StackEditor() {
options={{ minimap: { enabled: false }, fontSize: 13, tabSize: 2 }}
/>
) : (
<Editor
height="100%"
language="ini"
theme={theme === "dark" ? "vs-dark" : "light"}
value={env}
onChange={(v) => setEnv(v ?? "")}
options={{ minimap: { enabled: false }, fontSize: 13 }}
/>
<div className="h-full p-3">
<EnvEditor value={env} onChange={setEnv} />
</div>
)}
</div>
@@ -168,10 +184,21 @@ export function StackEditor() {
<Button variant="outline" onClick={() => save(false)} loading={saving}>
<Save className="h-4 w-4" /> Save Draft
</Button>
<Button onClick={() => save(true)} loading={saving}>
<Button onClick={onDeploy} loading={saving || checking}>
<Rocket className="h-4 w-4" /> Deploy
</Button>
</div>
{conflicts && (
<PortConflictDialog
conflicts={conflicts}
onContinue={() => {
setConflicts(null);
save(true);
}}
onCancel={() => setConflicts(null)}
/>
)}
</div>
);
}