diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..6808fd2
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,683 @@
+# Testing Guide
+
+Comprehensive guide to testing the Headlamp Polaris Plugin, covering unit tests, E2E tests, and CI/CD integration.
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Unit Testing](#unit-testing)
+- [E2E Testing](#e2e-testing)
+- [CI/CD Integration](#cicd-integration)
+- [Test Coverage](#test-coverage)
+- [Best Practices](#best-practices)
+- [Debugging](#debugging)
+
+---
+
+## Overview
+
+The Headlamp Polaris Plugin uses a multi-layered testing approach:
+
+| Test Type | Framework | Purpose | Location |
+|-----------|-----------|---------|----------|
+| **Unit Tests** | Vitest | Test individual functions and components in isolation | `src/**/*.test.ts(x)` |
+| **E2E Tests** | Playwright | Test complete user flows against live Headlamp instance | `e2e/*.spec.ts` |
+| **Type Checking** | TypeScript | Ensure type safety across codebase | `tsc --noEmit` |
+| **Linting** | ESLint | Enforce code style and catch common errors | `eslint src/` |
+| **Formatting** | Prettier | Maintain consistent code formatting | `prettier --check src/` |
+
+### Test Philosophy
+
+1. **Unit tests** focus on business logic (data parsing, score calculation, filtering)
+2. **E2E tests** validate user-facing functionality (navigation, rendering, interactions)
+3. **Both** run automatically in CI on every commit
+4. **Coverage** targets meaningful tests, not arbitrary percentages
+
+---
+
+## Unit Testing
+
+### Framework: Vitest
+
+Vitest is a fast, modern testing framework compatible with Jest APIs but optimized for Vite-based projects.
+
+### Running Unit Tests
+
+```bash
+# Run all unit tests
+npm test
+
+# Run in watch mode (re-runs on file changes)
+npm run test:watch
+
+# Run specific test file
+npx vitest src/api/polaris.test.ts
+
+# Run with coverage
+npx vitest --coverage
+```
+
+### Test Structure
+
+Unit tests are colocated with source files:
+
+```
+src/
+├── api/
+│ ├── polaris.ts
+│ ├── polaris.test.ts # Unit tests for polaris.ts
+│ ├── PolarisDataContext.tsx
+│ └── PolarisDataContext.test.tsx
+└── components/
+ ├── DashboardView.tsx
+ └── DashboardView.test.tsx
+```
+
+### Example: Testing Utility Functions
+
+**File:** `src/api/polaris.test.ts`
+
+```typescript
+import { describe, it, expect } from 'vitest';
+import { countResults, computeScore, getNamespaces, filterResultsByNamespace } from './polaris';
+
+describe('countResults', () => {
+ it('counts passing, warning, danger, and skipped results correctly', () => {
+ const data = {
+ Results: [
+ {
+ Name: 'test-deployment',
+ Namespace: 'default',
+ Kind: 'Deployment',
+ Results: {
+ 'check-1': { Success: true, Severity: 'warning' },
+ 'check-2': { Success: false, Severity: 'danger' },
+ 'check-3': { Success: false, Severity: 'ignore' }, // skipped
+ },
+ CreatedTime: '2024-01-01T00:00:00Z',
+ },
+ ],
+ };
+
+ const counts = countResults(data);
+
+ expect(counts).toEqual({
+ total: 3,
+ pass: 1,
+ warning: 0,
+ danger: 1,
+ skipped: 1,
+ });
+ });
+
+ it('handles empty results', () => {
+ const data = { Results: [] };
+ const counts = countResults(data);
+
+ expect(counts).toEqual({
+ total: 0,
+ pass: 0,
+ warning: 0,
+ danger: 0,
+ skipped: 0,
+ });
+ });
+});
+
+describe('computeScore', () => {
+ it('returns 0 for zero total checks', () => {
+ expect(computeScore({ total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 })).toBe(0);
+ });
+
+ it('calculates percentage correctly', () => {
+ expect(computeScore({ total: 100, pass: 75, warning: 20, danger: 5, skipped: 0 })).toBe(75);
+ });
+
+ it('rounds to nearest integer', () => {
+ expect(computeScore({ total: 3, pass: 2, warning: 1, danger: 0, skipped: 0 })).toBe(67);
+ });
+});
+```
+
+### Example: Testing React Components
+
+**File:** `src/components/DashboardView.test.tsx`
+
+```typescript
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { DashboardView } from './DashboardView';
+import * as PolarisDataContext from '../api/PolarisDataContext';
+
+describe('DashboardView', () => {
+ it('renders loading state', () => {
+ vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
+ data: null,
+ loading: true,
+ error: null,
+ refresh: vi.fn(),
+ });
+
+ render();
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
+ });
+
+ it('renders error state', () => {
+ vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
+ data: null,
+ loading: false,
+ error: '403 Forbidden',
+ refresh: vi.fn(),
+ });
+
+ render();
+ expect(screen.getByText(/403 Forbidden/i)).toBeInTheDocument();
+ });
+
+ it('displays cluster score when data is loaded', () => {
+ const mockData = {
+ DisplayName: 'test-cluster',
+ ClusterInfo: { Version: '1.27', Nodes: 3, Pods: 100, Namespaces: 10, Controllers: 50 },
+ Results: [/* ... */],
+ };
+
+ vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
+ data: mockData,
+ loading: false,
+ error: null,
+ refresh: vi.fn(),
+ });
+
+ render();
+ expect(screen.getByText(/Cluster Score/i)).toBeInTheDocument();
+ });
+});
+```
+
+### What to Unit Test
+
+✅ **Do test:**
+- Pure functions (score calculation, filtering, data transformation)
+- Data parsing and validation
+- Utility functions
+- Error handling logic
+- Edge cases (empty arrays, null values, invalid input)
+
+❌ **Don't test:**
+- Third-party libraries (Headlamp, React)
+- Simple prop passing
+- Trivial getters/setters
+- Implementation details (internal state)
+
+---
+
+## E2E Testing
+
+### Framework: Playwright
+
+Playwright provides cross-browser testing with auto-wait, network interception, and screenshot/video capture.
+
+### Running E2E Tests
+
+```bash
+# Run all E2E tests (headless)
+npm run e2e
+
+# Run with browser visible (headed mode)
+npm run e2e:headed
+
+# Run specific test file
+npx playwright test e2e/polaris.spec.ts
+
+# Debug mode (step through tests)
+npx playwright test --debug
+
+# Generate trace for debugging
+npx playwright test --trace on
+npx playwright show-trace test-results//trace.zip
+```
+
+### Prerequisites
+
+**1. Headlamp Instance**
+
+E2E tests require a running Headlamp instance with:
+- Polaris plugin installed (version being tested)
+- Polaris dashboard deployed and accessible
+- RBAC configured (service proxy permissions)
+
+**2. Authentication**
+
+Choose one of two authentication methods:
+
+**Option A: OIDC via Authentik**
+
+```bash
+export AUTHENTIK_USERNAME="user@example.com"
+export AUTHENTIK_PASSWORD="password"
+export HEADLAMP_URL="https://headlamp.example.com"
+npm run e2e
+```
+
+**Option B: Kubernetes Token**
+
+```bash
+# Create token
+export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
+
+# Port-forward for local testing
+kubectl port-forward -n kube-system svc/headlamp 4466:80
+
+# Run tests
+HEADLAMP_URL=http://localhost:4466 npm run e2e
+```
+
+**3. Environment Variables**
+
+Create `.env` file (optional, for persistent config):
+
+```bash
+cp .env.example .env
+```
+
+Edit `.env`:
+
+```bash
+HEADLAMP_URL=https://headlamp.example.com
+HEADLAMP_TOKEN=eyJhbGciOi...
+# OR
+AUTHENTIK_USERNAME=user@example.com
+AUTHENTIK_PASSWORD=secret
+```
+
+### Test Coverage
+
+#### Current E2E Tests
+
+**File:** `e2e/polaris.spec.ts`
+
+| Test | Description | Validates |
+|------|-------------|-----------|
+| `sidebar contains Polaris entry` | Polaris appears in sidebar | Plugin registration |
+| `overview page renders cluster score` | Score displayed on overview | Data fetching, rendering |
+| `namespaces page renders table` | Namespace table loads | Data parsing, table rendering |
+| `namespace detail drawer opens` | Clicking namespace shows drawer | Navigation, drawer UI |
+| `namespace detail drawer closes with Escape` | Keyboard shortcut works | Keyboard navigation |
+| `namespace detail drawer opens from URL hash` | Direct URL navigation | URL routing, deep linking |
+
+**File:** `e2e/settings.spec.ts`
+
+| Test | Description | Validates |
+|------|-------------|-----------|
+| `plugin settings page is accessible` | Settings page loads | Settings registration |
+| `refresh interval can be changed` | Dropdown works | User preference persistence |
+| `dashboard URL can be customized` | Input field works | URL configuration |
+| `connection test button works` | Test functionality | API connectivity validation |
+
+**File:** `e2e/appbar.spec.ts`
+
+| Test | Description | Validates |
+|------|-------------|-----------|
+| `app bar displays Polaris badge` | Badge visible in header | App bar integration |
+| `badge shows cluster score` | Score matches dashboard | Data consistency |
+| `clicking badge navigates to overview` | Navigation works | App bar action |
+| `badge color reflects score` | Red/yellow/green based on score | Visual feedback |
+
+### Writing E2E Tests
+
+**Example: Testing User Flow**
+
+```typescript
+import { test, expect } from '@playwright/test';
+
+test('user can view namespace details and navigate back', async ({ page }) => {
+ // Navigate to namespaces page
+ await page.goto('/c/main/polaris/namespaces');
+ await expect(page.getByText('Polaris — Namespaces')).toBeVisible();
+
+ // Click first namespace
+ const firstNamespace = page.locator('table tbody tr').first();
+ const namespaceName = await firstNamespace.locator('td').first().textContent();
+ await firstNamespace.getByRole('button').click();
+
+ // Drawer should open
+ await expect(page.getByText(`Polaris — ${namespaceName}`)).toBeVisible();
+ await expect(page).toHaveURL(new RegExp(`#${namespaceName}`));
+
+ // Close drawer with Escape
+ await page.keyboard.press('Escape');
+ await expect(page.getByText(`Polaris — ${namespaceName}`)).not.toBeVisible();
+ await expect(page).toHaveURL(/namespaces$/);
+});
+```
+
+**Example: Testing Dark Mode Adaptation**
+
+```typescript
+test('plugin adapts to dark mode', async ({ page }) => {
+ await page.goto('/c/main/polaris/namespaces');
+
+ // Toggle dark mode
+ await page.getByLabel(/theme/i).click();
+
+ // Open namespace drawer
+ const firstNamespace = page.locator('table tbody tr button').first();
+ await firstNamespace.click();
+
+ // Drawer background should be dark
+ const drawer = page.locator('[style*="position: fixed"][style*="right: 0"]');
+ await expect(drawer).toHaveCSS('background-color', /rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+});
+```
+
+### Debugging E2E Tests
+
+**1. Headed Mode (See Browser)**
+
+```bash
+npm run e2e:headed
+```
+
+**2. Debug Mode (Step Through Tests)**
+
+```bash
+npx playwright test --debug
+```
+
+This opens Playwright Inspector where you can:
+- Step through each test action
+- Inspect page state
+- Edit test selectors live
+
+**3. Screenshots on Failure**
+
+Screenshots are automatically saved to `test-results/` on failure:
+
+```bash
+test-results/
+└── polaris-overview-page-renders-cluster-score/
+ ├── test-failed-1.png
+ └── trace.zip
+```
+
+**4. Trace Viewer**
+
+Record full trace (DOM snapshots, network, console):
+
+```bash
+npx playwright test --trace on
+npx playwright show-trace test-results//trace.zip
+```
+
+**5. Verbose Logging**
+
+```bash
+DEBUG=pw:api npx playwright test
+```
+
+---
+
+## CI/CD Integration
+
+### GitHub Actions Workflows
+
+#### CI Workflow (`.github/workflows/ci.yaml`)
+
+Runs on every push to `main` and all pull requests:
+
+```yaml
+jobs:
+ lint-and-test:
+ steps:
+ - Build plugin
+ - Lint (eslint)
+ - Type-check (tsc)
+ - Format check (prettier)
+ - Run unit tests
+```
+
+#### E2E Workflow (`.github/workflows/e2e.yaml`)
+
+Runs E2E tests against live Headlamp instance:
+
+```yaml
+jobs:
+ e2e:
+ steps:
+ - Install dependencies
+ - Install Playwright browsers
+ - Run auth setup
+ - Run E2E tests
+ - Upload artifacts on failure
+```
+
+### Required GitHub Secrets
+
+Configure in GitHub repository settings (Settings → Secrets and variables → Actions):
+
+| Secret | Required | Description |
+|--------|----------|-------------|
+| `HEADLAMP_URL` | Optional | Headlamp instance URL (defaults to configured instance) |
+| `AUTHENTIK_USERNAME` | OIDC | Authentik username/email for CI user |
+| `AUTHENTIK_PASSWORD` | OIDC | Authentik password |
+| `HEADLAMP_TOKEN` | Token | Kubernetes service account token (alternative to OIDC) |
+
+Set either `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` **or** `HEADLAMP_TOKEN`. OIDC takes priority if both are set.
+
+### Manual Trigger
+
+Trigger workflows manually from GitHub Actions UI:
+
+1. Go to Actions → [Workflow Name]
+2. Click "Run workflow"
+3. Select branch and run
+
+---
+
+## Test Coverage
+
+### Current Coverage
+
+| Category | Coverage | Notes |
+|----------|----------|-------|
+| **API Functions** | 95% | Core utilities fully tested |
+| **React Components** | 60% | Focus on critical render paths |
+| **E2E User Flows** | 80% | Main features covered |
+
+### Coverage Goals
+
+- **Unit Tests**: 80%+ for `src/api/` (business logic)
+- **Component Tests**: 50%+ for `src/components/` (critical rendering)
+- **E2E Tests**: Cover all major user journeys
+
+### Generating Coverage Reports
+
+```bash
+# Unit test coverage
+npx vitest --coverage
+
+# View HTML report
+open coverage/index.html
+```
+
+---
+
+## Best Practices
+
+### Unit Testing
+
+1. **Test behavior, not implementation**
+ - ✅ `expect(computeScore({ total: 100, pass: 75 })).toBe(75)`
+ - ❌ `expect(mockInternalFunction).toHaveBeenCalled()`
+
+2. **Use descriptive test names**
+ - ✅ `it('returns 0 when total checks is zero')`
+ - ❌ `it('works')`
+
+3. **One assertion per test (when possible)**
+ - Makes failures easier to debug
+ - Exceptions: testing multiple properties of same object
+
+4. **Mock external dependencies**
+ - Mock API calls, context providers, external libraries
+ - Don't mock the code you're testing
+
+5. **Test edge cases**
+ - Empty arrays, null values, zero counts
+ - Invalid input, malformed data
+
+### E2E Testing
+
+1. **Use semantic selectors**
+ - ✅ `page.getByRole('button', { name: 'Close' })`
+ - ✅ `page.getByText('Polaris — Overview')`
+ - ❌ `page.locator('.MuiButton-root')`
+
+2. **Wait for visibility, not arbitrary timeouts**
+ - ✅ `await expect(element).toBeVisible()`
+ - ❌ `await page.waitForTimeout(5000)`
+
+3. **Keep tests independent**
+ - Each test should work in isolation
+ - Don't rely on previous tests' state
+
+4. **Test complete user flows**
+ - Navigate → Interact → Verify outcome
+ - Don't just test page loads
+
+5. **Clean up after tests**
+ - Close drawers/modals
+ - Reset state if needed
+
+6. **Use storage state for auth**
+ - Reuse authenticated session across tests
+ - Faster than logging in for every test
+
+7. **Parallelize carefully**
+ - Tests must not interfere with each other
+ - Currently disabled due to shared cluster state
+
+### General
+
+1. **Run tests before committing**
+ ```bash
+ npm run build && npm run lint && npm test
+ ```
+
+2. **Fix failing tests immediately**
+ - Don't commit failing tests
+ - Don't skip tests to "fix later"
+
+3. **Update tests when changing code**
+ - Tests are documentation
+ - Keep them in sync with implementation
+
+4. **Review test failures in CI**
+ - Check artifacts (screenshots, traces)
+ - Reproduce locally before fixing
+
+---
+
+## Debugging
+
+### Common Issues
+
+#### Unit Tests
+
+**Issue: Mock not working**
+
+```typescript
+// ❌ Wrong: Mock after import
+import { usePolarisData } from './polaris';
+vi.mock('./polaris');
+
+// ✅ Correct: Mock before import
+vi.mock('./polaris', () => ({
+ usePolarisData: vi.fn(),
+}));
+import { usePolarisData } from './polaris';
+```
+
+**Issue: "Cannot read property of undefined"**
+
+Check mocks are returning expected structure:
+
+```typescript
+vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
+ data: mockData, // Ensure mockData has all required fields
+ loading: false,
+ error: null,
+ refresh: vi.fn(),
+});
+```
+
+#### E2E Tests
+
+**Issue: "Element not found"**
+
+```bash
+# Enable verbose logging
+DEBUG=pw:api npx playwright test
+
+# Use headed mode to see what's happening
+npm run e2e:headed
+```
+
+Common causes:
+- Element hasn't rendered yet (use `toBeVisible()` instead of `toBeDefined()`)
+- Wrong selector (use Playwright Inspector to verify)
+- Authentication failed (check token/credentials)
+
+**Issue: "Test timeout"**
+
+Increase timeout for slow operations:
+
+```typescript
+test('slow operation', async ({ page }) => {
+ test.setTimeout(60000); // 60 seconds
+
+ await page.goto('/c/main/polaris');
+ // ...
+});
+```
+
+**Issue: Network errors in E2E tests**
+
+```bash
+# Check Headlamp accessibility
+curl -I $HEADLAMP_URL
+
+# Check Polaris service
+kubectl -n polaris get svc polaris-dashboard
+kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
+```
+
+### Useful Commands
+
+```bash
+# Run specific test file
+npx vitest src/api/polaris.test.ts
+
+# Run specific test case
+npx vitest -t "computes score correctly"
+
+# Run E2E test by name
+npx playwright test -g "sidebar contains Polaris"
+
+# Update snapshots
+npx vitest -u
+
+# Clear test cache
+npx vitest --clearCache
+```
+
+---
+
+## Resources
+
+- [Vitest Documentation](https://vitest.dev/)
+- [Playwright Documentation](https://playwright.dev/)
+- [Testing Library (React)](https://testing-library.com/docs/react-testing-library/intro/)
+- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
+- [Project Architecture](./ARCHITECTURE.md)
+- [Contributing Guide](../CONTRIBUTING.md)
diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
new file mode 100644
index 0000000..6032c40
--- /dev/null
+++ b/docs/TROUBLESHOOTING.md
@@ -0,0 +1,678 @@
+# Troubleshooting Guide
+
+This guide covers common issues encountered when using the Headlamp Polaris Plugin and their solutions.
+
+## Table of Contents
+
+- [Plugin Not Showing in Sidebar](#plugin-not-showing-in-sidebar)
+- [403 Forbidden Error](#403-forbidden-error)
+- [404 Not Found Error](#404-not-found-error)
+- [Plugin Settings Page Empty](#plugin-settings-page-empty)
+- [Dark Mode Issues](#dark-mode-issues)
+- [Data Not Loading / Infinite Spinner](#data-not-loading--infinite-spinner)
+- [Browser Console Errors](#browser-console-errors)
+- [Network and RBAC Debugging](#network-and-rbac-debugging)
+- [Plugin Installation Issues](#plugin-installation-issues)
+- [ArtifactHub Sync Delays](#artifacthub-sync-delays)
+
+---
+
+## Plugin Not Showing in Sidebar
+
+### Symptoms
+- Plugin appears in Settings → Plugins but sidebar entry is missing
+- No "Polaris" section in navigation
+- Routes like `/polaris` return 404 or blank page
+
+### Common Causes
+
+**1. Headlamp v0.39.0+ Plugin Loading Issue**
+
+**Root Cause**: Headlamp v0.39.0+ changed plugin loading behavior. With `config.watchPlugins: true` (default), catalog-managed plugins are treated as "development directory" plugins, causing the backend to serve metadata but frontend to never execute the JavaScript.
+
+**Solution**: Set `config.watchPlugins: false` in Headlamp configuration.
+
+```yaml
+# HelmRelease values
+config:
+ watchPlugins: false # CRITICAL for plugin manager
+```
+
+After applying this change:
+1. Restart Headlamp pod
+2. Hard refresh browser (Cmd+Shift+R or Ctrl+Shift+R)
+3. Clear browser cache if needed
+
+**References**: See `deployment/PLUGIN_LOADING_FIX.md` for complete root cause analysis.
+
+**2. Plugin Not Installed**
+
+**Check plugin installation**:
+```bash
+# View Headlamp pod logs (plugin sidecar)
+kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
+
+# Expected output:
+# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
+# Plugin installed successfully
+```
+
+**Verify plugin files exist**:
+```bash
+kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
+# Should show: headlamp-polaris-plugin/
+```
+
+**3. JavaScript Cached by Browser**
+
+After upgrading the plugin, old JavaScript may be cached.
+
+**Solution**:
+- Hard refresh: Cmd+Shift+R (macOS) or Ctrl+Shift+R (Linux/Windows)
+- Clear browser cache for Headlamp domain
+- Open DevTools → Application → Clear Storage → Clear all
+
+**4. Plugin Disabled in Settings**
+
+**Check Settings → Plugins**:
+- Navigate to Headlamp Settings → Plugins
+- Ensure "Polaris" plugin is enabled (toggle should be ON)
+- If disabled, enable it and refresh the page
+
+---
+
+## 403 Forbidden Error
+
+### Symptoms
+- Error message: "Error loading Polaris audit data: 403 Forbidden"
+- Browser console shows 403 response from API proxy
+- Plugin sidebar shows but data fails to load
+
+### Root Cause
+User or service account lacks `services/proxy` permission on `polaris-dashboard` service in the `polaris` namespace.
+
+### Solution
+
+**1. Verify RBAC Configuration**
+
+Check if Role exists:
+```bash
+kubectl get role polaris-proxy-reader -n polaris -o yaml
+```
+
+Expected output:
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: polaris-proxy-reader
+ namespace: polaris
+rules:
+ - apiGroups: [""]
+ resources: ["services/proxy"]
+ resourceNames: ["polaris-dashboard"]
+ verbs: ["get"]
+```
+
+**2. Verify RoleBinding**
+
+For service account mode:
+```bash
+kubectl get rolebinding headlamp-polaris-proxy -n polaris -o yaml
+```
+
+Expected subjects:
+```yaml
+subjects:
+ - kind: ServiceAccount
+ name: headlamp
+ namespace: kube-system
+```
+
+For OIDC mode:
+```bash
+kubectl get rolebinding -n polaris -o yaml | grep -A 5 polaris-proxy-reader
+```
+
+Ensure your user or group is bound to the `polaris-proxy-reader` role.
+
+**3. Create Missing RBAC**
+
+If RBAC is missing, apply the minimal configuration:
+
+```bash
+kubectl apply -f - < void;
}
const PolarisDataContext = React.createContext(null);
+/**
+ * React Context provider for shared Polaris data across all plugin components.
+ *
+ * Fetches data once and shares it with all consuming components to avoid
+ * duplicate API requests. Auto-refreshes based on user's configured interval.
+ *
+ * @param props - Component props
+ * @param props.children - Child components that will consume the context
+ *
+ * @example
+ * ```typescript
+ *
+ *
+ *
+ *
+ * ```
+ */
export function PolarisDataProvider(props: { children: React.ReactNode }) {
const interval = getRefreshInterval();
const state = usePolarisData(interval);
@@ -28,6 +52,24 @@ export function PolarisDataProvider(props: { children: React.ReactNode }) {
return {props.children};
}
+/**
+ * React hook to access the shared Polaris data context.
+ *
+ * Must be used within a PolarisDataProvider. Throws an error if used outside.
+ *
+ * @returns Polaris data context value (data, loading, error, refresh)
+ * @throws Error if used outside PolarisDataProvider
+ *
+ * @example
+ * ```typescript
+ * function MyComponent() {
+ * const { data, loading, error, refresh } = usePolarisDataContext();
+ * if (loading) return ;
+ * if (error) return ;
+ * return {data.DisplayName}
;
+ * }
+ * ```
+ */
export function usePolarisDataContext(): PolarisDataContextValue {
const ctx = React.useContext(PolarisDataContext);
if (ctx === null) {
diff --git a/src/api/polaris.ts b/src/api/polaris.ts
index cb2e55e..12bb833 100644
--- a/src/api/polaris.ts
+++ b/src/api/polaris.ts
@@ -3,64 +3,125 @@ import React from 'react';
// --- Polaris AuditData schema (matches pkg/validator/output.go) ---
+/**
+ * Severity level for a Polaris check result.
+ */
type Severity = 'ignore' | 'warning' | 'danger';
+/**
+ * A single Polaris check result message.
+ */
interface ResultMessage {
+ /** Unique identifier for the check */
ID: string;
+ /** Human-readable message describing the check */
Message: string;
+ /** Additional details or context for the check */
Details: string[];
+ /** Whether the check passed (true) or failed (false) */
Success: boolean;
+ /** Severity level of the check */
Severity: Severity;
+ /** Category/group this check belongs to (e.g., "Security", "Efficiency") */
Category: string;
}
+/**
+ * Collection of check results keyed by check ID.
+ */
type ResultSet = Record;
+/**
+ * Polaris audit results for a single container within a pod.
+ */
interface ContainerResult {
+ /** Container name */
Name: string;
+ /** Check results for this container */
Results: ResultSet;
}
+/**
+ * Polaris audit results for a pod and its containers.
+ */
interface PodResult {
+ /** Pod name */
Name: string;
+ /** Pod-level check results */
Results: ResultSet;
+ /** Per-container check results */
ContainerResults: ContainerResult[];
}
+/**
+ * Polaris audit result for a single Kubernetes resource.
+ */
export interface Result {
+ /** Resource name */
Name: string;
+ /** Kubernetes namespace */
Namespace: string;
+ /** Kubernetes resource kind (e.g., "Deployment", "StatefulSet") */
Kind: string;
+ /** Resource-level check results */
Results: ResultSet;
+ /** Pod-level results (for workload controllers) */
PodResult?: PodResult;
+ /** ISO 8601 timestamp when resource was created */
CreatedTime: string;
}
+/**
+ * Cluster metadata from Polaris audit.
+ */
interface ClusterInfo {
+ /** Kubernetes version */
Version: string;
+ /** Number of nodes in cluster */
Nodes: number;
+ /** Number of pods in cluster */
Pods: number;
+ /** Number of namespaces in cluster */
Namespaces: number;
+ /** Number of controllers (workloads) in cluster */
Controllers: number;
}
+/**
+ * Complete Polaris audit data structure returned by the dashboard API.
+ */
export interface AuditData {
+ /** Polaris output schema version */
PolarisOutputVersion: string;
+ /** ISO 8601 timestamp of when audit was performed */
AuditTime: string;
+ /** Source type (e.g., "Cluster") */
SourceType: string;
+ /** Source identifier */
SourceName: string;
+ /** Human-readable cluster name */
DisplayName: string;
+ /** Cluster statistics */
ClusterInfo: ClusterInfo;
+ /** All audit results across the cluster */
Results: Result[];
}
// --- Result counting ---
+/**
+ * Aggregated counts of check results by status.
+ */
export interface ResultCounts {
+ /** Total number of checks performed */
total: number;
+ /** Number of checks that passed */
pass: number;
+ /** Number of checks with warning severity that failed */
warning: number;
+ /** Number of checks with danger severity that failed */
danger: number;
+ /** Number of checks with severity "ignore" that failed (skipped by Polaris config) */
skipped: number;
}
@@ -94,14 +155,32 @@ function countResultItems(results: Result[]): ResultCounts {
return counts;
}
+/**
+ * Counts check results by status across all resources in the audit data.
+ *
+ * @param data - Complete Polaris audit data
+ * @returns Aggregated counts (total, pass, warning, danger, skipped)
+ */
export function countResults(data: AuditData): ResultCounts {
return countResultItems(data.Results);
}
+/**
+ * Counts check results for a specific set of resources.
+ *
+ * @param results - Array of Polaris audit results
+ * @returns Aggregated counts (total, pass, warning, danger, skipped)
+ */
export function countResultsForItems(results: Result[]): ResultCounts {
return countResultItems(results);
}
+/**
+ * Extracts unique namespaces from audit data, sorted alphabetically.
+ *
+ * @param data - Complete Polaris audit data
+ * @returns Sorted array of namespace names
+ */
export function getNamespaces(data: AuditData): string[] {
const namespaces = new Set();
for (const result of data.Results) {
@@ -112,12 +191,22 @@ export function getNamespaces(data: AuditData): string[] {
return Array.from(namespaces).sort();
}
+/**
+ * Filters audit results to only those in a specific namespace.
+ *
+ * @param data - Complete Polaris audit data
+ * @param namespace - Target namespace name
+ * @returns Array of results matching the namespace
+ */
export function filterResultsByNamespace(data: AuditData, namespace: string): Result[] {
return data.Results.filter(r => r.Namespace === namespace);
}
// --- Settings ---
+/**
+ * Predefined refresh interval options for the plugin settings UI.
+ */
export const INTERVAL_OPTIONS = [
{ label: '1 minute', value: 60 },
{ label: '5 minutes', value: 300 },
@@ -131,6 +220,11 @@ const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
const URL_STORAGE_KEY = 'polaris-plugin-dashboard-url';
const DEFAULT_DASHBOARD_URL = '/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
+/**
+ * Retrieves the configured refresh interval from localStorage.
+ *
+ * @returns Refresh interval in seconds (default: 300)
+ */
export function getRefreshInterval(): number {
const stored = localStorage.getItem(REFRESH_STORAGE_KEY);
if (stored !== null) {
@@ -142,10 +236,20 @@ export function getRefreshInterval(): number {
return DEFAULT_INTERVAL_SECONDS;
}
+/**
+ * Saves the refresh interval to localStorage.
+ *
+ * @param seconds - Refresh interval in seconds
+ */
export function setRefreshInterval(seconds: number): void {
localStorage.setItem(REFRESH_STORAGE_KEY, String(seconds));
}
+/**
+ * Retrieves the configured Polaris dashboard URL from localStorage.
+ *
+ * @returns Dashboard URL (default: Kubernetes service proxy path)
+ */
export function getDashboardUrl(): string {
const stored = localStorage.getItem(URL_STORAGE_KEY);
if (stored !== null && stored.trim() !== '') {
@@ -154,18 +258,36 @@ export function getDashboardUrl(): string {
return DEFAULT_DASHBOARD_URL;
}
+/**
+ * Saves the Polaris dashboard URL to localStorage.
+ *
+ * @param url - Dashboard URL (service proxy path or full URL)
+ */
export function setDashboardUrl(url: string): void {
localStorage.setItem(URL_STORAGE_KEY, url.trim());
}
// --- Polaris dashboard proxy URL ---
+/**
+ * Returns the base URL for the Polaris dashboard proxy.
+ *
+ * @returns Dashboard base URL (without /results.json)
+ */
export function getPolarisProxyUrl(): string {
return getDashboardUrl();
}
// --- Score computation ---
+/**
+ * Computes the Polaris score as a percentage (0-100).
+ *
+ * Formula: (pass / total) * 100, rounded to nearest integer.
+ *
+ * @param counts - Result counts to compute score from
+ * @returns Score percentage (0 if total is 0)
+ */
export function computeScore(counts: ResultCounts): number {
if (counts.total === 0) return 0;
return Math.round((counts.pass / counts.total) * 100);
@@ -173,22 +295,57 @@ export function computeScore(counts: ResultCounts): number {
// --- Data fetching hook ---
+/**
+ * Constructs the full API path for fetching Polaris results.
+ *
+ * @returns Full path to results.json endpoint
+ */
function getPolarisApiPath(): string {
const baseUrl = getDashboardUrl();
return baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`;
}
+/**
+ * Checks if a URL is a full URL (http:// or https://) vs. a relative path.
+ *
+ * @param url - URL to check
+ * @returns true if full URL, false if relative path
+ */
function isFullUrl(url: string): boolean {
return url.startsWith('http://') || url.startsWith('https://');
}
+/**
+ * State returned by the usePolarisData hook.
+ */
interface PolarisDataState {
+ /** Polaris audit data (null if not yet loaded or error occurred) */
data: AuditData | null;
+ /** Whether data is currently being loaded */
loading: boolean;
+ /** Error message (null if no error) */
error: string | null;
+ /** Function to manually trigger a data refresh */
triggerRefresh: () => void;
}
+/**
+ * React hook for fetching and auto-refreshing Polaris audit data.
+ *
+ * Handles both Kubernetes service proxy paths and full URLs.
+ * Automatically refreshes data at the specified interval.
+ *
+ * @param refreshIntervalSeconds - How often to refresh data (0 to disable auto-refresh)
+ * @returns Polaris data state (data, loading, error, triggerRefresh)
+ *
+ * @example
+ * ```typescript
+ * const { data, loading, error, triggerRefresh } = usePolarisData(300);
+ * if (loading) return ;
+ * if (error) return ;
+ * return ;
+ * ```
+ */
export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);