feat: Removed comments
Server Deploy / deploy (push) Failing after 1m59s

This commit is contained in:
2026-07-24 09:51:30 +01:00
parent 3b52bcbeb8
commit 1a6cf03c03
94 changed files with 772 additions and 937 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
const inputClass =
"w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder-text-secondary/50 focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent";
// Name of the ClusterSecretStore the generated manifests reference.
const STORE_NAME = "vantage-store";
function CopyBlock({ label, yaml }: { label: string; yaml: string }) {
+13 -13
View File
@@ -34,7 +34,7 @@ export default function ServerConsolePage() {
const [zoom, setZoom] = useState(1);
const dprRef = useRef(1);
// Inject the vendored Guacamole client script once.
useEffect(() => {
const s = document.createElement("script");
s.src = "/lib/guacamole-common.js";
@@ -45,7 +45,7 @@ export default function ServerConsolePage() {
};
}, []);
// Disconnect on unmount.
useEffect(() => {
return () => {
connectionRef.current?.disconnect();
@@ -90,8 +90,8 @@ export default function ServerConsolePage() {
}
const { token, ws_path } = await api.connectConsole(body);
// Defer the actual openConsole until after the form is unmounted so the
// container measures at full height (see effect below).
setPending({ token, wsPath: ws_path });
setConnected(true);
} catch (e) {
@@ -101,8 +101,8 @@ export default function ServerConsolePage() {
}
}
// Runs after `connected` flips and the connection form is gone, so the
// container now occupies its full flex height.
useEffect(() => {
if (!connected || !pending || !containerRef.current) return;
@@ -111,9 +111,9 @@ export default function ServerConsolePage() {
const rect = containerRef.current.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
dprRef.current = dpr;
// Request the remote at device-pixel resolution with a fixed 96 dpi, then
// scale the display back down by dpr. Folding dpr into `dpi` instead makes
// the remote enlarge everything, which reads as a zoomed-in view.
const connectData =
`token=${encodeURIComponent(pending.token)}` +
`&width=${Math.floor(rect.width * dpr)}` +
@@ -125,10 +125,10 @@ export default function ServerConsolePage() {
setPending(null);
}, [connected, pending]);
// Apply zoom live without reconnecting: resize the remote to a resolution
// that, once scaled to fit the container, yields the requested zoom. Higher
// zoom = fewer remote pixels rendered larger. Display always fits the
// container exactly, so no scrollbars appear.
useEffect(() => {
if (!connectionRef.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
@@ -10,7 +10,7 @@ const inputClass =
"w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30";
const labelClass = "mb-1.5 block text-sm font-medium text-text-secondary";
// Config fields required per channel type.
const CONFIG_FIELDS: Record<ChannelType, string[]> = {
webhook: ["url"],
slack: ["url"],
+6 -6
View File
@@ -53,8 +53,8 @@ function MembersCard() {
const { mutate: changeRole, error: roleError } = useMutation({
mutationFn: ({ userId, next }: { userId: string; next: Role }) => api.updateOrgUserRole(userId, next),
onSuccess: invalidate,
// A rejected change (last owner, owner-only grant) leaves the select showing
// the value the server refused — refetch so the row snaps back to the truth.
onError: invalidate,
});
@@ -65,8 +65,8 @@ function MembersCard() {
const actionError = (roleError ?? removeError) as Error | null;
// The server lets only an owner grant or change the owner role. Mirror that
// here so admins aren't offered controls that can only 403.
const isOwner = user?.role === "owner";
const assignableRoles = isOwner ? ROLES : ROLES.filter((r) => r !== "owner");
@@ -112,7 +112,7 @@ function MembersCard() {
<Tbody>
{users.map((u: OrgUser) => {
const isSelf = u.user_id === user?.user_id;
// Own row stays read-only, and only owners may act on owners.
const locked = isSelf || (u.role === "owner" && !isOwner);
return (
<Tr key={u.user_id}>
@@ -242,7 +242,7 @@ function OIDCCard() {
setIssuer(cfg.issuer ?? "");
setClientId(cfg.client_id ?? "");
setEnabled(cfg.enabled);
// The secret is never returned; leave the field blank to mean "unchanged".
setClientSecret("");
}, [cfg]);
+3 -3
View File
@@ -139,7 +139,7 @@ export default function SettingsPage() {
const queryClient = useQueryClient();
const { isAdmin } = useAuth();
// /api/settings requires owner|admin and 403s for members, so don't even ask.
const { data: settings, isLoading } = useQuery({
queryKey: ["settings"],
queryFn: api.getSettings,
@@ -168,8 +168,8 @@ export default function SettingsPage() {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!settings) return;
// Preserve legacy alert/email values (managed via Notification Channels now);
// only the offline threshold and log retention are edited here.
save({
alerts: { ...settings.alerts, offline_threshold_minutes: thresholdMinutes },
email: settings.email,
+26 -26
View File
@@ -23,9 +23,9 @@ function AdhocBadge() {
return <span className="rounded px-1.5 py-0.5 font-mono text-[10px] uppercase bg-signal/15 text-signal">ad-hoc</span>;
}
// Stable snapshot of only the fields the editor controls. Excludes volatile
// server-echo fields (e.g. updated_at) that would otherwise change on every
// save and cause autosave to loop forever.
function snapshotOf(w: Workflow): string {
return JSON.stringify({
name: w.name,
@@ -81,11 +81,11 @@ export default function WorkflowBuilder() {
setWf(loaded);
savedSnapshotRef.current = snapshotOf(loaded);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loaded]);
// Lazily fetch the keys for every secret group so the inspector's
// secret-ref checklist can offer "group/KEY" options.
useEffect(() => {
if (!secretGroups) return;
secretGroups.forEach((g: SecretGroupSummary) => {
@@ -101,17 +101,17 @@ export default function WorkflowBuilder() {
setGroupKeys((prev) => ({ ...prev, [g.group]: [] }));
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [secretGroups]);
// Keep a ref to the latest workflow so an in-flight save can tell whether
// the user edited again while the request was on the wire.
wfRef.current = wf;
// Autosave: debounce 800ms after any change to the workflow (step added,
// removed, reordered, or edited) and persist. Diffing the serialized state
// against the last saved snapshot skips no-op saves and the initial load.
// Must stay above the early return below so hook order is stable.
useEffect(() => {
if (!wf || savedSnapshotRef.current === null) return;
if (snapshotOf(wf) === savedSnapshotRef.current) return;
@@ -119,10 +119,10 @@ export default function WorkflowBuilder() {
save();
}, 800);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wf]);
// Re-render every 15s so the "Saved … ago" label stays current.
useEffect(() => {
if (!lastSaved) return;
const iv = setInterval(() => setTick((n) => n + 1), 15000);
@@ -141,8 +141,8 @@ export default function WorkflowBuilder() {
const selectedIdxInWf = selectedRef ? wf.steps.indexOf(selectedRef) : -1;
const save = async () => {
// Never run two saves concurrently: a request in flight would race the
// next one. The finally block re-triggers if edits landed meanwhile.
if (savingRef.current) return;
const current = wfRef.current;
if (!current) return;
@@ -158,14 +158,14 @@ export default function WorkflowBuilder() {
return;
}
if (wfRef.current && snapshotOf(wfRef.current) === snapshot) {
// Nothing changed while the request was in flight: adopt the
// server echo as the new saved baseline.
savedSnapshotRef.current = snapshotOf(updated);
setWf(updated);
} else {
// The user edited again mid-flight. Keep their newer state and
// mark only the SENT snapshot as saved, so the effect re-fires
// and persists the remaining changes.
savedSnapshotRef.current = snapshot;
}
setLastSaved(new Date());
@@ -174,8 +174,8 @@ export default function WorkflowBuilder() {
} finally {
savingRef.current = false;
setSaving(false);
// If edits arrived during the save (or a concurrent save was
// skipped), persist them on the next tick.
if (wfRef.current && snapshotOf(wfRef.current) !== savedSnapshotRef.current) {
setTimeout(() => save(), 0);
}
@@ -581,8 +581,8 @@ export default function WorkflowBuilder() {
open={editWorkflowOpen}
workflow={wf}
onSaved={(w) => {
// The modal already persisted w; sync the snapshot so
// autosave doesn't fire a redundant follow-up save.
savedSnapshotRef.current = snapshotOf(w);
setWf(w);
}}
@@ -6,7 +6,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { api, ServerRun, StepRun, WorkflowRun } from "@/lib/api";
import { Button } from "@/components/ui";
// ---- status vocabulary ----------------------------------------------------
type CellKind = "done" | "fail" | "run" | "wait" | "skip" | "warn";
@@ -23,7 +23,7 @@ function cellKind(status: string): CellKind {
case "cancelled":
return "warn";
default:
return "wait"; // queued / pending / missing
return "wait";
}
}
@@ -45,7 +45,7 @@ const cellClass: Record<CellKind, string> = {
warn: "bg-warning/15 text-warning",
};
// ---- run-level status pill ------------------------------------------------
type PillKind = "running" | "success" | "failed" | "neutral";
@@ -84,7 +84,7 @@ function StatusPill({ status, small }: { status: string; small?: boolean }) {
);
}
// ---- time helpers ---------------------------------------------------------
function fmtDuration(ms: number): string {
if (ms < 0) ms = 0;
@@ -104,7 +104,7 @@ function stepDuration(st: StepRun, running: boolean, now: number): string {
return fmtDuration(end - start);
}
// ---- live log terminal ----------------------------------------------------
function LogTerminal({ runId, server }: { runId: string; server: ServerRun }) {
const [text, setText] = useState("");
@@ -155,9 +155,9 @@ function LogTerminal({ runId, server }: { runId: string; server: ServerRun }) {
);
}
// LogLines renders the raw server-run log, parsing each line's leading UTC
// timestamp ([2026-07-20T12:04:02.000Z]) and rendering it in the viewer's local
// timezone. Event markers (===== …) are highlighted so the run's shape scans.
const TS_RE = /^\[(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\]\s?(.*)$/;
function LogLines({ text }: { text: string }) {
@@ -189,7 +189,7 @@ function LogLines({ text }: { text: string }) {
);
}
// ---- step list ------------------------------------------------------------
function StepList({ server, now }: { server: ServerRun; now: number }) {
const running = server.status === "running";
@@ -228,7 +228,7 @@ function StepList({ server, now }: { server: ServerRun; now: number }) {
);
}
// ---- execution matrix (signature) -----------------------------------------
interface Column {
order: number;
@@ -295,7 +295,7 @@ function ExecutionMatrix({ run, columns, selected, onSelect }: { run: WorkflowRu
);
}
// ---- page -----------------------------------------------------------------
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
@@ -320,7 +320,7 @@ export default function RunDetail() {
const running = run?.status === "running";
// tick the elapsed clock while running
useEffect(() => {
if (!running) return;
const t = setInterval(() => setNow(Date.now()), 1000);
@@ -329,7 +329,7 @@ export default function RunDetail() {
const columns = useMemo(() => (run ? buildColumns(run) : []), [run]);
// default selection: first running server, else first server
const selectedServer = useMemo(() => {
if (!run || run.server_runs.length === 0) return null;
if (selected) {
+5 -5
View File
@@ -9,9 +9,9 @@ export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// If the org has no users yet, first-run setup is the only way in. And if the
// visitor already has a valid session on this host, the form is a dead end —
// send them into the app instead.
useEffect(() => {
(async () => {
try {
@@ -21,13 +21,13 @@ export default function LoginPage() {
return;
}
} catch {
// Status unavailable — fall through and let the login form stand.
}
try {
await auth.me();
window.location.href = "/";
} catch {
// Not signed in (or session invalid here) — show the form.
}
})();
}, []);
+4 -4
View File
@@ -30,7 +30,7 @@ function orgLoginUrlForSlug(slug: string): string {
if (rest[0] !== "vantage") return "/login";
const newHost = [slug, ...rest].join(".") + (port ? `:${port}` : "");
return `${protocol}//${newHost}/login`;
return `${protocol}
}
export default function SetupPage() {
@@ -41,7 +41,7 @@ export default function SetupPage() {
const [validationError, setValidationError] = useState<string | null>(null);
const [created, setCreated] = useState<{ slug: string; loginUrl: string } | null>(null);
// Setup is a one-shot route; once an owner exists it must not be reachable.
useEffect(() => {
auth
.bootstrapStatus()
@@ -49,7 +49,7 @@ export default function SetupPage() {
if (!s.needs_setup) window.location.href = "/login";
})
.catch(() => {
// Status unavailable — let the form stand; the backend re-checks on submit.
});
}, []);
@@ -74,7 +74,7 @@ export default function SetupPage() {
bootstrap();
}
// Prefer the backend's message (it owns the real validation rules).
const message = validationError ?? (error ? (error as Error).message : null);
const inputClass =
+4 -4
View File
@@ -51,10 +51,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
window.location.href = "/login";
return;
}
// Anything else (backend unreachable, org host mismatch) leaves us with
// no session. Rendering children here would mount the whole shell with
// user=null — every page would fire its own doomed API calls and the UI
// would read as a member view. Show the failure instead.
setError((err as Error).message || "Unable to load your session.");
setLoading(false);
}
+3 -3
View File
@@ -105,7 +105,7 @@ export function Sidebar() {
const visibleItems = navItems.filter((item) => !item.adminOnly || isAdmin);
// Longest match wins, so /settings/org doesn't also light up /settings.
const activeHref = visibleItems.reduce<string | null>((best, item) => {
const matches = pathname === item.href || pathname.startsWith(item.href + "/");
if (!matches) return best;
@@ -113,11 +113,11 @@ export function Sidebar() {
}, null);
async function handleLogout() {
// /auth/logout is POST-only on the server.
try {
await auth.logout();
} catch {
// Fall through — clearing the client-side session view is what matters.
}
window.location.href = "/login";
}
+2 -2
View File
@@ -17,8 +17,8 @@ export function EditStepModal({ open, step, onClose }: { open: boolean; step: Wo
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// NOTE: because state is seeded from props, render the modal conditionally
// (parent mounts it only when opening) OR key it by step_id so it re-seeds.
const save = async () => {
setBusy(true); setError(null);