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:
co-authored by
Claude Opus 4.8
parent
b553c1b861
commit
22d9864436
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user