Dashboard: programs list deployments; Tools page; statics on Services; editable launcher
- Programs page: each card lists the program's deployments (name · kind) instead of a single kind badge; kind-filter chips removed (nav is the kind lens now). protonmail honestly shows tool + job. - New Tools nav page (Wrench) — programs with a tool deployment (/programs?kind=tool). - Services page now shows statics too: /services returns service + static, and a static renders as a caddy-served card (KindBadge, served URL, no start/stop). - Program detail: DeploymentsSection + deleteConfirm rebuilt over program.deployments; header drops the (now nonexistent) program kind. - ServiceFields / JobFields: launcher is now an editable select (python|command| container|compose|node), persisted into run.launcher via the normal save. clean pnpm build + tsc --noEmit.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useState } from "react"
|
||||
import type { ServiceDetail } from "@/types"
|
||||
import { launcherLabel } from "@/lib/labels"
|
||||
import { Field, TextField, FormFooter, useEnvSecrets } from "./fields"
|
||||
|
||||
interface Props {
|
||||
@@ -12,6 +11,31 @@ interface Props {
|
||||
type Obj = Record<string, unknown>
|
||||
const obj = (v: unknown): Obj => (v as Obj) ?? {}
|
||||
|
||||
// The systemd launch mechanisms (a service is manager=systemd). Editable, so a
|
||||
// mis-set launcher can be corrected; the primary "Runs" target maps per launcher.
|
||||
const LAUNCHERS = ["python", "command", "container", "compose", "node"]
|
||||
|
||||
/** Fold the "Runs" text into the run block for the chosen launcher, preserving
|
||||
* any other run fields (args, ports, package_manager, …) already present. */
|
||||
function applyLauncher(run: Obj, launcher: string, target: string): Obj {
|
||||
const out: Obj = { ...run, launcher }
|
||||
const t = target.trim()
|
||||
if (launcher === "command") {
|
||||
out.argv = t.split(/\s+/).filter(Boolean)
|
||||
delete out.program
|
||||
} else if (launcher === "python") {
|
||||
out.program = t
|
||||
delete out.argv
|
||||
} else if (launcher === "container") {
|
||||
if (t) out.image = t
|
||||
} else if (launcher === "node") {
|
||||
if (t) out.script = t
|
||||
} else if (launcher === "compose") {
|
||||
if (t) out.file = t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Edit a service's deployment config (run / expose / proxy / env). */
|
||||
export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
const m = service.manifest
|
||||
@@ -23,8 +47,14 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const [description, setDescription] = useState((m.description as string) ?? "")
|
||||
const [launcher, setLauncher] = useState((run.launcher as string) ?? "python")
|
||||
const [runProgram, setRunProgram] = useState(
|
||||
(run.program as string) ?? ((run.argv as string[]) ?? []).join(" "),
|
||||
(run.program as string) ||
|
||||
((run.argv as string[]) ?? []).join(" ") ||
|
||||
(run.image as string) ||
|
||||
(run.script as string) ||
|
||||
(run.file as string) ||
|
||||
"",
|
||||
)
|
||||
const [port, setPort] = useState(internal.port != null ? String(internal.port) : "")
|
||||
const [health, setHealth] = useState((httpExpose.health_path as string) ?? "")
|
||||
@@ -33,8 +63,6 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
|
||||
const { element: envEditor, merged } = useEnvSecrets(obj(obj(m.defaults).env) as Record<string, string>)
|
||||
|
||||
const launcher = (run.launcher as string) ?? "?"
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setSaved(false)
|
||||
@@ -43,12 +71,8 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
delete config.id
|
||||
config.description = description || undefined
|
||||
|
||||
// Only python/command launchers are edited here; other launchers
|
||||
// (container/node/compose) keep their original run block untouched.
|
||||
const runOut = obj(config.run)
|
||||
if (launcher === "command") runOut.argv = runProgram.split(" ").filter(Boolean)
|
||||
else if (launcher === "python") runOut.program = runProgram
|
||||
config.run = runOut
|
||||
// Rebuild the run block for the chosen launcher, preserving other fields.
|
||||
config.run = applyLauncher(obj(config.run), launcher, runProgram)
|
||||
|
||||
if (port) {
|
||||
config.expose = {
|
||||
@@ -81,14 +105,27 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
<TextField label="Description" value={description} onChange={setDescription} />
|
||||
<Field
|
||||
label="Runs"
|
||||
hint="The console script (python runner) or command this service executes."
|
||||
hint="How this service starts: the launcher, and its target — a console script (python), a command/argv (command), an image (container), a script (node), or a compose file (compose)."
|
||||
>
|
||||
<span className="text-sm font-mono text-[var(--muted)]">{launcherLabel(launcher)} · </span>
|
||||
<input
|
||||
value={runProgram}
|
||||
onChange={(e) => setRunProgram(e.target.value)}
|
||||
className="w-full sm:w-56 bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={launcher}
|
||||
onChange={(e) => setLauncher(e.target.value)}
|
||||
className="bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]"
|
||||
>
|
||||
{LAUNCHERS.map((l) => (
|
||||
<option key={l} value={l}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-[var(--muted)]">·</span>
|
||||
<input
|
||||
value={runProgram}
|
||||
onChange={(e) => setRunProgram(e.target.value)}
|
||||
className="w-full sm:w-56 bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<TextField
|
||||
label="Port"
|
||||
|
||||
Reference in New Issue
Block a user