Require explicit user action before Applications/Diffs run a search or diff fan-out
Context and Problem Statement
routes/diffs/+page.svelte fetched applications on load() and then, in a client-side $effect, always
called fetchDiffs() — which fans out one diff-generation POST per application across every ArgoCD instance
(lib/backend/diffs.ts's buildDiffRequests, fully parallel via Promise.all, no concurrency cap). If the
page loaded without a targetRef query parameter — which happens on a bare visit to /diffs, e.g. via the nav
bar link — buildDiffRequests silently fell back to comparing each application's own live ref against itself.
That is a request per application, to every ArgoCD instance, to produce a diff that is always empty by
construction: pure waste, and at scale it means clicking the wrong nav link "hammers" every connected ArgoCD
for nothing.
routes/applications/+page.svelte had a milder version of the same shape: load() always fetched the
applications list immediately, regardless of whether the user had specified any label filter, so landing on
the tab ran an unfiltered "show everything" search by default rather than waiting for intent.
Decision Drivers
- The diffs fan-out is the expensive case (N requests, each triggering real manifest generation against
ArgoCD/git) and was firing on a code path — a bare nav-link click — that has no
targetRefto give it, by construction. This isn't a rare misuse; it's the default outcome of the most obvious way to reach the page. - The home page (
routes/+page.svelte) already had alabels/excludeLabels(/targetRef) form pattern for both actions — reusing it instead of inventing a second UI idiom for the same idea kept the fix consistent rather than one-off. +page.ts'sload()for both routes only ever readslabels/excludeLabels(diffs also reads nothing for the fan-out itself —targetRefis read client-side); the actual expensive work is entirely client-side and triggerable independently ofload(), which is what makes gating it there — without touchingload()— the narrowest fix available.
Considered Options
- Leave both routes as-is and rely on users not bookmarking or nav-clicking into a bare
/diffs. - Gate only the diffs page (the objectively expensive N-way fan-out) and leave
/applications's single, comparatively cheap GET running immediately on load. - Gate both routes behind the same explicit-submission pattern: extract the home page's two forms into shared
ApplicationsForm/DiffsFormcomponents, and show the relevant one on each route until the user submits it (or arrives with the relevant filter already in the URL, e.g. from the home page).
Decision Outcome
Chosen option: gate both routes behind shared, reused form components. lib/ui/components/ApplicationsForm.svelte
and DiffsForm.svelte hold their own submit-button-disabled-until-valid state (the target ref requirement on
Diffs, previously enforced with a noRefSpecified toast, is now enforced by the submit button simply staying
disabled — "design, not text") and call an onSubmit callback the parent route wires to its own navigation:
routes/diffs/+page.svelte: showsDiffsFormwhenevertargetRefis absent from the URL; the$effectbails out before callingfetchDiffsin that case, so zero diff requests fire. Submitting callsgoto()with the newtargetRef/labels in the query string.routes/applications/+page.svelte: showsApplicationsFormunless the URL already indicates an explicit search — either a reallabels/excludeLabelsvalue, or asearched=truemarker. That marker exists specifically becauselabels/excludeLabelsare legitimately optional:buildQuery(lib/backend/url.ts) drops empty values, so a search submitted with both fields blank produces a URL indistinguishable from a bare nav-link click unless something else marks it as submitted. This follows the same precedent as the page's existingrefresh=truequery parameter.- The home page's own two forms became instances of these same components, so all three routes share one implementation of "ask before you search."
Consequences
- Good, because the diffs page can no longer fan out N requests to every ArgoCD instance as a side effect of navigation — it always requires an explicit, non-empty target ref first.
- Good, because all three routes (
/,/applications,/diffs) now present the same form for the same action, instead of the home page's version being the only "real" one. - Good, because
+page.ts'sload()functions and their tests (load.spec.ts) needed no changes — the fix is entirely about when the client-side effect/render decides to act, not what the loaders fetch. - Neutral, because
load()still fetches the applications list unconditionally on/applicationseven while the form is showing (it's a single cheap GET, and having it ready removes a second load-then-fetch round trip once the user does submit) — the gate is on rendering/acting, not on that particular request. - Bad, because the
searched=truemarker is a small URL wart specific to the applications route's optional filters; it has no equivalent need on the diffs route, wheretargetRefitself is a sufficient, always non-empty signal once submitted.
Pros and Cons of the Options
Leave as-is
- Good, because zero effort.
- Bad, because it leaves the actual reported problem (bare nav click into
/diffsfans out N requests to every ArgoCD instance) in place.
Gate only Diffs
- Good, because it fixes the objectively expensive case with a smaller diff.
- Bad, because it leaves
/applicationsbehaviorally inconsistent with/diffsfor no reason other than one being cheaper — the request was explicitly for both to behave "logically the same."
Gate both via shared form components (chosen)
See Decision Outcome.
More Information
- Implementation plan: Gate Applications/Diffs behind explicit search
- Related: ADR 0007 (the visual-consistency pass from the same review; this ADR covers a distinct, functional decision about request timing rather than appearance)