Skip to content

perf: virtualize log rendering, cache API data, harden WS/backend - #19

Merged
Esysc merged 7 commits into
mainfrom
feature/optimization
Aug 10, 2026
Merged

Esysc merged 7 commits into
mainfrom
feature/optimization

Conversation

@Esysc

@Esysc Esysc commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

Performance and hardening pass driven by a review of the previous agent feedback. Focus is on the two real bottlenecks (log rendering and repeated API fetches), plus leaks/security issues found on deeper review.

Changes

Frontend

  • LogViewer now virtualizes rows: only the visible window (+overscan) is in the DOM, so per-message rendering is constant regardless of buffered log count. Auto-follow only when pinned to the tail.
  • New cachedFetch() utility: in-memory cache with TTL + in-flight dedup. Namespaces/contexts/nodes cached 60s, pods/containers 8s. Shared across all stream panels and the header.
  • useWebSocket: socket closed on unmount (fixes stream/connection leak when switching views), parse-failure entries buffered while paused, append path avoids double array copy, logs carry stable monotonic ids used as render keys.

Backend

  • WebSocket CheckOrigin allowlist: same-origin or ALLOWED_ORIGINS env (dev at localhost:5173). Prevents arbitrary web pages from reaching this backend's kubectl/stern surface.
  • kubectl namespace filters use --namespace= form so a value starting with - can't be parsed as a flag.
  • Single Kubernetes client cached per context and shared across streams and cluster endpoints (one credential refresher per context instead of per connection).

Tests / checks

  • Replaced allow-all-origin test with same-origin/allowed/rejected cases.
  • Added compileLogFilters unit coverage.
  • go vet, go build, Go tests, frontend lint (76 tests) all green.

Release note: no API/CLI changes; ALLOWED_ORIGINS is additive for deployments that proxy from another host.

Esysc and others added 2 commits August 8, 2026 07:48
- StreamLog LogViewer rows so DOM work stays constant regardless of buffer size
- Add cached/deduplicated API fetch for autocomplete and autocomplete data
- Add WebSocket cleanup on stream unmount to prevent connection leaks
- Buffer parse-failure entries while paused
- Add stable log ids to avoid row remounts on ring-buffer rollover
- Precompile client-side log filters once per log change
- Enforce WebSocket origin allowlist (same-origin or ALLOWED_ORIGINS)
- Reuse a single Kubernetes client per context across streams
- Use --namespace= value form so kubectl args cannot be flag-injected
…-reconnect

- App.jsx: validate stored context against fetched list, not just falsy check
- useAutoComplete: cleanup targets correct localStorage key (stern-ui-cluster)
- LogViewer: remove select-none so log lines are copyable
- LogFilters: fix double-unwrapped onChange breaking search/level inputs
- LogFilters: wire into StreamPanel (was exported but never rendered)
- StreamPanel: use ?? instead of || for query default so cleared pods stay cleared
- StreamPanel: add query to auto-reconnect check on pod change
- logUtils: default unrecognized logs to info instead of unknown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR focuses on improving UI performance and hardening backend/frontend behavior for streaming logs and cluster data access by reducing DOM/render cost, deduplicating repeated API fetches, and tightening WebSocket/backend handling.

Changes:

  • Virtualizes log rendering and improves stream log buffering/append behavior to keep UI responsive under large log volumes.
  • Adds a shared in-memory cachedFetch() with TTL + in-flight deduplication and wires it into autocomplete and initial context loading.
  • Hardens the backend WebSocket origin policy and reuses Kubernetes clients per context; updates kubectl namespace flag usage to avoid flag-injection.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
main.go Adds WS origin allowlist, refactors until-time parsing, caches kube clients per context, and hardens kubectl namespace args
main_test.go Updates WebSocket origin policy tests to cover allow/reject cases
frontend/src/utils/logUtils.js Precompiles client-side filters to avoid per-log regex compilation
frontend/src/utils/cache.js Introduces cached fetch with TTL + in-flight request deduplication
frontend/src/utils/tests/logUtils.test.js Adds unit coverage for compileLogFilters
frontend/src/test/setup.js Adds ResizeObserver mock for jsdom tests
frontend/src/hooks/useWebSocket.js Fixes socket lifecycle leak, buffers parse failures while paused, and adds monotonic log ids
frontend/src/hooks/useAutoComplete.js Switches autocomplete requests to cachedFetch() with TTLs
frontend/src/components/stream/StreamPanel.jsx Uses precompiled filters to reduce per-update filtering overhead
frontend/src/components/logs/LogViewer.jsx Implements virtualized log row rendering with tail-follow behavior
frontend/src/App.jsx Switches initial contexts load to cachedFetch()
CHANGELOG.md Documents the performance and hardening changes

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread frontend/src/components/logs/LogViewer.jsx
Comment thread main.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

main.go:310

  • startCredentialRefresher(context.Background(), ...) creates a per-context goroutine that never stops. In environments where users can switch across many contexts over time, this can leak goroutines and cached clients indefinitely (no eviction/lifecycle), which is a reliability concern for long-lived backend processes.
	startCredentialRefresher(context.Background(), entry.clientset, contextName, &entry.mu)

frontend/src/components/logs/LogViewer.jsx:138

  • Virtualization assumes every rendered row is exactly ROW_HEIGHT tall, but the row markup doesn't currently enforce a 20px height/line-height. If CSS or font metrics change (or any wrapping/padding sneaks in), scroll math can drift and the wrong rows will be shown.
                      className="flex gap-2 hover:bg-gray-900/50 whitespace-nowrap"

frontend/src/App.jsx:34

  • The initial context selection clears to '' when /api/contexts fails (because the list becomes []), even if a previously stored context exists. This can cause the app to lose the last-known context during transient backend errors; consider falling back to the stored value when the fetched list is empty.
        if (stored && list.includes(stored)) {
          setContext(stored);
        } else {
          setContext(list[0] || '');
        }

frontend/src/utils/cache.js:33

  • cachedFetch() is new shared infrastructure (TTL handling, in-flight dedup, error cache invalidation) but there are no unit tests covering its caching semantics. Given there are existing utility tests in frontend/src/utils/__tests__, adding coverage here would help prevent regressions.
export function cachedFetch(url, { ttl = 30_000 } = {}) {
  const now = Date.now();
  const entry = cache.get(url);

  if (entry && entry.expiresAt > now) {
    return Promise.resolve(entry.value);
  }

  if (entry && entry.promise) {
    return entry.promise;
  }

  const promise = apiFetch(url)
    .then((value) => {
      cache.set(url, { value, expiresAt: Date.now() + ttl });
      return value;
    })
    .catch((err) => {
      cache.delete(url);
      throw err;
    });

  cache.set(url, { promise });
  return promise;
}

@Esysc
Esysc merged commit 6ab3416 into main Aug 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants