From 2751b02428b4a897b22c2d49b8dbb5f6953d8da8 Mon Sep 17 00:00:00 2001
From: Lukasz Raczylo
Date: Fri, 2 Oct 2026 02:49:13 +0100
Subject: [PATCH] docs: rewrite the README and update the feature list
The README now has a feature table, install steps, a flag table, a
config reference and troubleshooting. It documents tui.columns, typed
namespace entry, shell completion and the --context flag.
It drops the comparison table with other tools, which the repository
cannot back up, and corrects the supported resource types and the Go
version.
---
README.md | 635 ++++++++++++++++++++----------------------------------
1 file changed, 228 insertions(+), 407 deletions(-)
diff --git a/README.md b/README.md
index 26f851b..e01f2fc 100644
--- a/README.md
+++ b/README.md
@@ -8,81 +8,63 @@
-
- Kubernetes port-forward manager with interactive terminal UI
-
+# kportal
-kportal manages multiple Kubernetes port-forwards with an interactive terminal interface. It provides real-time status updates, automatic reconnection, hot-reload configuration, and mDNS hostname publishing.
+kportal runs many Kubernetes port-forwards from one YAML file and shows their status in a terminal UI. It reconnects dropped forwards, reloads the file when it changes, and can run without the UI.

-## ✨ Features
+Website:
-- **Interactive TUI** - Terminal interface with keyboard navigation
-- **Live management** - Add, edit, and delete port-forwards without restarting
-- **Auto-reconnect** - Exponential backoff retry on connection failures
-- **Hot-reload** - Configuration changes applied automatically
-- **Health monitoring** - Multiple check methods with stale connection detection
-- **Multi-context** - Support for multiple Kubernetes contexts and namespaces
-- **Pod restart handling** - Automatic reconnection when pods restart
-- **Label selectors** - Dynamic pod targeting using label selectors
-- **Port conflict detection** - Validates port availability with PID information
-- **mDNS hostnames** - Access forwards via `.local` hostnames
-- **HTTP traffic logging** - Real-time HTTP request/response logging for debugging
-- **Connection benchmarking** - Built-in HTTP benchmarking with latency statistics
-- **Headless mode** - Background operation for scripting and automation
+## Features
-## 🔄 Comparison with Other Tools
+| Area | What kportal does |
+|------|-------------------|
+| Forwards | Forwards to `service/`, `pod/` (exact name or name prefix), or `pod` with a label `selector`. Only TCP is supported. |
+| Terminal UI | Add (`n`), edit (`e`), delete (`d`) and toggle (`Space`) forwards without a restart. The add wizard writes the config file. |
+| Table columns | `tui.columns` sets which columns the forwards table shows, their order and their width. It applies to the UI and to the `-verbose` table. |
+| Add wizard | If the cluster does not allow listing namespaces, the wizard asks you to type the namespace. |
+| Reconnect | Retries with exponential backoff: 1s, 2s, 4s, 8s, then 10s, with 10% jitter. It does not stop retrying. |
+| Pod restarts | Forwards by pod prefix or selector connect to a new pod after a restart. |
+| Health checks | `tcp-dial` or `data-transfer` check every 3s by default. A connection is stale when it is older than `maxConnectionAge` and idle, or idle longer than `maxIdleTime`. |
+| Hot reload | The file watcher and `SIGHUP` reload the config. An invalid config is rejected and the previous one stays active. |
+| Contexts | Several Kubernetes contexts and namespaces in one file. |
+| Port conflicts | Startup and reload checks report a busy local port and the process that holds it. |
+| mDNS | With `mdns.enabled: true`, each forward is reachable as `.local`. Without an alias, the resource name is used. |
+| HTTP log | Per-forward request and response log in the UI, with detail view, filter, search, JSON formatting, gzip/deflate decoding and clipboard copy. Header values such as `Authorization` and `Cookie` are redacted. |
+| Benchmark | Press `b` to send HTTP requests through a forward. Reports success and failure counts, min/max/avg latency, P50/P95/P99, requests per second and status codes. |
+| Generate | `kportal generate` lists services in a cluster and appends the ones you pick to the config with consecutive local ports. |
+| Headless | `-headless` runs without the UI and logs to stderr. `-log-format json` switches the log format. |
+| Import | `--convert` turns a kftray JSON file into a kportal YAML file. |
+| Shell completion | `kportal completion` prints a bash, zsh or fish completion script, or installs it. |
+| Updates | `-update` checks for a newer release. `-version` prints the version. |
-| Feature | kportal | [k9s](https://k9scli.io/) | [Kube Forwarder](https://kube-forwarder.pixelpoint.io/) | [kftray](https://kftray.app/) |
-|---------|---------|------|----------------|--------|
-| **Interface** | Terminal TUI | Terminal TUI | Desktop GUI (Electron) | Desktop GUI + TUI |
-| **Persistent Config** | ✅ YAML file | ❌ Session only | ✅ JSON bookmarks | ✅ JSON + Git sync |
-| **Auto-reconnect** | ✅ Exponential backoff | ❌ Manual | ✅ Basic | ✅ Watch API |
-| **Hot-reload Config** | ✅ File watch + SIGHUP | ❌ | ❌ | ❌ |
-| **Health Checks** | ✅ TCP + data-transfer | ❌ | ❌ | ❌ |
-| **Stale Connection Detection** | ✅ Age + idle tracking | ❌ | ❌ | ❌ |
-| **HTTP Traffic Logging** | ✅ Built-in viewer | ❌ | ❌ | ✅ |
-| **Connection Benchmarking** | ✅ Built-in | ✅ Via Hey | ❌ | ❌ |
-| **mDNS Hostnames** | ✅ `.local` domains | ❌ | ❌ | ❌ |
-| **Label Selectors** | ✅ | ✅ | ❌ | ✅ |
-| **Multi-context** | ✅ | ✅ | ✅ | ✅ |
-| **Headless Mode** | ✅ | ❌ | ❌ | ❌ |
-| **System Tray** | ❌ | ❌ | ❌ | ✅ |
-| **UDP Support** | ❌ | ❌ | ❌ | ✅ Proxy relay |
-| **Dependencies** | Single binary | Single binary | Electron | Tauri + kubectl |
+## Install
-## 📦 Installation
-
-### Homebrew (macOS)
+Homebrew (macOS):
```bash
brew install --cask lukaszraczylo/taps/kportal
```
-> **Note**: If you previously installed via `brew install lukaszraczylo/taps/kportal` (formula), uninstall first:
-> ```bash
-> brew uninstall kportal
-> ```
+If you installed the formula (`brew install lukaszraczylo/taps/kportal`) earlier, run `brew uninstall kportal` first.
-### Quick Install
+Install script:
```bash
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
```
-The installer downloads `kportal--checksums.txt` from the same release and verifies the archive's SHA-256 before installing. If [`cosign`](https://github.com/sigstore/cosign) is on your `PATH`, the checksums file's keyless cosign signature is also verified against the shared-actions reusable workflow identity.
+The script downloads `kportal--checksums.txt` from the same release and verifies the archive SHA-256. If [`cosign`](https://github.com/sigstore/cosign) is on `PATH`, it also verifies the keyless signature of the checksums file.
| Variable | Effect |
|----------|--------|
-| `DRY_RUN=1` | Download and verify only; do not install |
-| `SKIP_COSIGN=1` | Skip cosign signature verification (SHA-256 is still enforced) |
+| `DRY_RUN=1` | Download and verify only. Do not install. |
+| `SKIP_COSIGN=1` | Skip the cosign check. The SHA-256 check still runs. |
-### Manual Download
+Manual download: the [releases page](https://github.com/lukaszraczylo/kportal/releases) has archives for Linux, macOS and Windows on amd64 and arm64.
-Download binaries from the [releases page](https://github.com/lukaszraczylo/kportal/releases).
-
-### Build from Source
+Build from source (Go 1.26 or later, see `go.mod`):
```bash
git clone https://github.com/lukaszraczylo/kportal.git
@@ -90,12 +72,10 @@ cd kportal
make build && make install
```
-### Verifying Release Signatures
-
-All release checksums are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify:
+### Verify a release signature
```bash
-# Download the checksum file and its sigstore bundle from the release
+# Download the checksums file and its sigstore bundle from the release
cosign verify-blob \
--certificate-identity-regexp "^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
@@ -103,7 +83,7 @@ cosign verify-blob \
kportal--checksums.txt
```
-## 🚀 Quick Start
+## Quick start
Create `.kportal.yaml`:
@@ -124,7 +104,7 @@ contexts:
port: 8080
localPort: 8080
alias: api
- httpLog: true # Enable HTTP traffic logging
+ httpLog: true
```
Run:
@@ -133,209 +113,55 @@ Run:
kportal
```
-### Keyboard Controls
+## CLI
-| Key | Action |
-|-----|--------|
-| `↑↓` / `j/k` | Navigate |
-| `Space` / `Enter` | Toggle forward |
-| `n` | Add new forward |
-| `e` | Edit forward |
-| `d` | Delete forward |
-| `b` | Benchmark connection |
-| `l` | View HTTP logs |
-| `q` | Quit |
+| Command or flag | Effect |
+|-----------------|--------|
+| `kportal` | Start the terminal UI. |
+| `-c ` | Config file. Default `.kportal.yaml`. |
+| `-v` | Verbose logging. In headless mode it sets the log level only. |
+| `-headless` | No UI. Logs go to stderr. |
+| `-check` | Validate the config and exit. |
+| `-context ` | Forward only the named contexts. See [Select contexts](#select-contexts). |
+| `-log-format text\|json` | Log format. Default `text`. |
+| `-convert ` | Convert a kftray JSON file to kportal YAML. |
+| `-convert-output ` | Output of `-convert`. Default `.kportal.yaml`. |
+| `-update` | Check for updates and exit. |
+| `-version` | Print the version and exit. |
+| `kportal generate` | Add forwards from a cluster. See [Generate](#generate-forwards-from-a-cluster). |
+| `kportal completion` | Print a shell completion script. |
-## 📖 Configuration
+Each flag also accepts the double-dash form, for example `--check`.
-### Basic Structure
-
-```yaml
-contexts:
- - name:
- namespaces:
- - name:
- forwards:
- - resource: /
- protocol: tcp
- port:
- localPort:
- alias: # optional
- selector: # optional
- httpLog: true # optional - enable HTTP logging
-```
-
-### Forward Options
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `resource` | Yes | Resource type and name (e.g., `service/postgres`, `pod/my-app`) |
-| `protocol` | Yes | Protocol (`tcp`) |
-| `port` | Yes | Remote port |
-| `localPort` | Yes | Local port |
-| `alias` | No | Display name and mDNS hostname |
-| `selector` | No | Label selector for pod resolution |
-| `httpLog` | No | Enable HTTP traffic logging (`true`/`false`) |
-
-### Resource Formats
-
-| Format | Description |
-|--------|-------------|
-| `service/name` | Service forwarding |
-| `pod/name` | Direct pod by name |
-| `pod/prefix` | Pod by prefix (matches `prefix-*`) |
-| `pod` + `selector` | Pod by label selector |
-| `deployment/name` | Deployment |
-
-### Health Check Configuration
-
-```yaml
-healthCheck:
- interval: "3s" # Check frequency
- timeout: "2s" # Check timeout
- method: "data-transfer" # tcp-dial or data-transfer
- maxConnectionAge: "25m" # Reconnect before k8s timeout
- maxIdleTime: "10m" # Detect idle connections
-
-reliability:
- tcpKeepalive: "30s"
- dialTimeout: "30s"
- retryOnStale: true
-```
-
-Health check methods:
-- `tcp-dial` - Fast TCP connection test
-- `data-transfer` - Verifies tunnel functionality by attempting data read
-
-Connection age reconnection only triggers when the connection is also idle, preventing interruption of active transfers like database dumps.
-
-### mDNS Hostnames
-
-Enable mDNS to access forwards via `.local` hostnames:
-
-```yaml
-mdns:
- enabled: true
-
-contexts:
- - name: production
- namespaces:
- - name: default
- forwards:
- - resource: service/postgres
- port: 5432
- localPort: 5432
- alias: prod-db # Accessible via prod-db.local:5432
-```
-
-- Explicit `alias` becomes `.local`
-- Without alias, hostname is generated from resource name (`service/redis` → `redis.local`)
-- Works on macOS (Bonjour) and Linux (avahi-daemon)
-
-Verify registration:
-```bash
-dns-sd -B _kportal._tcp local # macOS
-avahi-browse -t _kportal._tcp # Linux
-```
-
-### Table Columns
-
-Choose which columns the forwards table shows, and in what order. The setting applies to the interactive UI and the `-verbose` table:
-
-```yaml
-tui:
- columns:
- - name: alias
- width: 30 # optional, 1-200
- - name: resource
- - name: local
- - name: status
-```
-
-- Valid names: `context`, `namespace`, `alias`, `type`, `resource`, `remote`, `local`, `status`
-- The list order is the render order, and unlisted columns are hidden
-- `width` is the maximum text width, from 1 to 200. If you omit it, the column keeps its default width
-- If you omit `tui`, the table keeps its default columns and order
-- Changes apply on hot-reload
-
-## Usage
-
-### Interactive Mode
-
-```bash
-kportal
-```
-
-### Verbose Mode
-
-```bash
-kportal -v
-```
-
-### Headless Mode
-
-Run without TUI for scripting and automation:
-
-```bash
-kportal -headless
-```
-
-Headless mode emits both structured and standard-library logs to stderr by default
-(suitable for redirecting to a log file or systemd journal). The `-v` flag controls
-log level (debug vs info), not destination.
-
-Combines well with verbose mode for background operation:
+Headless example:
```bash
kportal -headless -v 2>kportal.log &
```
-### Validate Configuration
+Completion:
```bash
-kportal --check
+kportal completion --shell zsh # print the script
+kportal completion --shell zsh --install # install it
+kportal completion --uninstall # remove it
```
-### Custom Config File
+Without `--shell`, kportal detects the shell.
-```bash
-kportal -c /path/to/config.yaml
-```
+### Select contexts
-### Select Contexts
-
-By default every context in the config file is forwarded. `--context` limits the
-run to the contexts you name, which is useful when the file describes several
-clusters but you only work against one at a time:
+By default kportal forwards every context in the file. `--context` limits the run to the contexts you name:
```bash
kportal --context development-team-a
-```
-
-The flag is repeatable and accepts comma-separated values, so these are
-equivalent:
-
-```bash
kportal --context team-a --context team-b
kportal --context team-a,team-b
```
-It composes with the other modes, and works well as a per-cluster shell alias:
+An unknown name is an error that lists the contexts in the file. Those are the `contexts[].name` values in `.kportal.yaml`, not every context in your kubeconfig.
-```bash
-alias kp-a='kportal --context development-team-a'
-kportal --context development-team-a -headless
-```
-
-An unknown context name is an error that lists the contexts the file defines, so
-a typo fails loudly instead of silently forwarding nothing.
-
-#### Reusing local ports across contexts
-
-Local-port conflicts are only reported between forwards that could run at the
-same time. Contexts that are never selected together may therefore reuse the
-same `localPort`, so the same service keeps the same local port in every
-cluster and the app you are developing never needs reconfiguring:
+Local-port conflicts are only reported between forwards that can run together. With `--context`, contexts you never select together can reuse a `localPort`:
```yaml
contexts:
@@ -357,15 +183,9 @@ contexts:
- <<: *rate-service # same local port, no conflict
```
-This requires passing `--context`. Without it every context is active, so the
-duplicate ports above would genuinely collide and are still reported as an
-error - as is naming both contexts at once (`--context team-a,team-b`).
+Without `--context`, or with both contexts named, the duplicate port is an error.
-### Generate Forwards from a Cluster
-
-The `generate` subcommand discovers services in a Kubernetes context and lets you
-interactively pick which ones to forward. Selected entries are appended to the
-config file with consecutive local ports starting from a value you choose.
+### Generate forwards from a cluster
```bash
kportal generate --context=my-cluster
@@ -375,97 +195,137 @@ kportal generate --context=my-cluster --dry-run
| Flag | Description |
|------|-------------|
-| `--context` | (required) Kubernetes context to scan |
-| `--config` | Path to kportal config file (default: `.kportal.yaml`) |
-| `--dry-run` | Print the planned forwards but do not modify the config |
+| `--context` | Required. Kubernetes context to scan. |
+| `--config` | Config file to append to. Default `.kportal.yaml`. |
+| `--dry-run` | Print the planned forwards. Do not change the config. |
-The interactive flow has three steps:
+Steps:
-1. **Namespaces** — multi-select with `space`, toggle-all with `a`, filter with `/`.
-2. **Services** — same controls; rows already present in the config are locked off, and non-TCP ports are skipped (UDP is not supported by kportal's forward layer).
-3. **Port assignment** — choose a starting local port (default `10000`, must be ≥ `1024`). Local ports are assigned consecutively in stable order, skipping any already in use.
+1. Namespaces: select with `space`, toggle all with `a`, filter with `/`.
+2. Services: same keys. Services already in the config are locked. Non-TCP ports are skipped.
+3. Port assignment: choose a first local port (default `10000`, minimum `1024`). Ports are assigned in order and skip ports in use.
-Press `enter` on the final step to save (or to print and exit when `--dry-run` is set), `b` to go back, or `esc` to cancel.
+Press `enter` on the last step to save (or print, with `--dry-run`), `b` to go back, `esc` to cancel.
-## Status Indicators
+## Keys
-| Indicator | Description |
-|-----------|-------------|
+Forwards list:
+
+| Key | Action |
+|-----|--------|
+| `Up` `Down` / `j` `k` | Move the selection |
+| `PgUp` `PgDn` / `Ctrl+u` `Ctrl+d` | Move by 10 rows |
+| `Space` / `Enter` | Toggle the forward |
+| `n` | Add a forward |
+| `e` | Edit the forward |
+| `d` | Delete the forward (asks to confirm) |
+| `b` | Benchmark the forward |
+| `l` | Open the HTTP log |
+| `q` / `Ctrl+c` | Quit |
+
+In the add and edit wizard, `h` on the confirmation step toggles `httpLog`. Advanced `httpLog` keys you set in YAML stay unchanged.
+
+Status values:
+
+| Indicator | Meaning |
+|-----------|---------|
| `● Active` | Connection healthy |
-| `○ Starting` | Initial connection (10s grace period) |
-| `◐ Reconnecting` | Reconnecting after failure |
+| `○ Starting` | First connection. Failed health checks show Starting for the first 10s. |
+| `◐ Reconnecting` | Reconnecting after a failure |
| `✗ Error` | Connection failed |
-| `○ Disabled` | Manually disabled |
+| `○ Disabled` | Turned off in the UI |
-## Advanced Features
+## Configuration
-### HTTP Traffic Logging
+### Forwards
-Press `l` in the TUI to view real-time HTTP traffic for a selected forward. The log viewer shows:
+```yaml
+contexts:
+ - name:
+ namespaces:
+ - name:
+ forwards:
+ - resource: /
+ protocol: tcp
+ port:
+ localPort:
+ alias: # optional
+ selector: # optional
+ httpLog: true # optional
+```
-| Column | Description |
-|--------|-------------|
-| TIME | Request timestamp |
-| METHOD | HTTP method (GET, POST, etc.) |
-| STATUS | Response status code |
-| LATENCY | Request duration |
-| PATH | Request path |
+| Field | Required | Description |
+|-------|----------|-------------|
+| `resource` | Yes | `service/`, `pod/`, or `pod` with `selector` |
+| `protocol` | No | `tcp`. No other value is accepted. |
+| `port` | Yes | Remote port |
+| `localPort` | Yes | Local port |
+| `alias` | No | Display name and mDNS hostname |
+| `selector` | With bare `pod` | Label selector. Not allowed with `pod/`. |
+| `httpLog` | No | `true`, or a map of options (see [HTTP log](#http-log)) |
-**List view shortcuts:**
+### Health checks and reliability
-| Key | Action |
-|-----|--------|
-| `↑/↓` | Navigate entries |
-| `Enter` | View request details |
-| `g/G` | Jump to top/bottom |
-| `a` | Toggle auto-scroll |
-| `f` | Cycle filter mode (All → Non-2xx → Errors) |
-| `/` | Search by path or method |
-| `c` | Clear all filters |
-| `q` | Close log viewer |
+```yaml
+healthCheck:
+ interval: "3s"
+ timeout: "2s"
+ method: "data-transfer" # or tcp-dial
+ maxConnectionAge: "25m"
+ maxIdleTime: "10m"
-**Detail view:**
+reliability:
+ tcpKeepalive: "30s"
+ dialTimeout: "30s"
+ retryOnStale: true
+ watchdogPeriod: "30s"
+```
-Press `Enter` on any entry to see full request/response details including:
-- Request and response headers (alphabetically sorted)
-- Request and response bodies
-- Timing information and status codes
+The values above are the defaults from `.kportal.yaml`.
-| Key | Action |
-|-----|--------|
-| `↑/↓` | Scroll content |
-| `PgUp/PgDn` | Scroll by page |
-| `g` | Jump to top |
-| `c` | Copy response body to clipboard |
-| `Esc/q` | Return to list |
+- `tcp-dial` opens a TCP connection to the local port.
+- `data-transfer` also tries to read data through the tunnel. It is the default.
+- A connection past `maxConnectionAge` reconnects only when it is also idle, so a running transfer is not cut.
-**Body display features:**
-- **JSON formatting** - JSON bodies are pretty-printed with syntax highlighting
-- **Compression handling** - gzip/deflate content is automatically decompressed
-- **Binary detection** - Binary content shows a placeholder instead of garbled data
+### mDNS
-**Filter modes:**
-- **All** - Show all entries
-- **Non-2xx** - Hide successful (2xx) responses
-- **Errors** - Show only 4xx and 5xx responses
+```yaml
+mdns:
+ enabled: true
+```
-**Toggling per-forward logging:**
+- `alias: prod-db` publishes `prod-db.local`.
+- Without an alias, `service/redis` publishes `redis.local`.
+- A bare `pod` with a selector and no alias is not published.
-In the add/edit wizard, press `h` on the confirmation step to toggle `httpLog` on or
-off for the current forward. The wizard preserves any advanced `httpLog` keys
-(`logFile`, `includeHeaders`, `maxBodySize`, `filterPath`) you set in YAML.
+Check registration:
-**Header redaction:**
+```bash
+dns-sd -B _kportal._tcp local # macOS
+avahi-browse -t _kportal._tcp # Linux
+```
-When `httpLog.includeHeaders: true` is set, sensitive header values are
-automatically replaced with `[REDACTED]`. The header name is preserved so you can
-see that an `Authorization` header was present without exposing its value. Redacted
-headers include `Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`,
-`X-Auth-Token`, `X-Csrf-Token`, `Proxy-Authorization`, `X-Access-Token`, and any
-header whose name contains `token`, `secret`, `password`, or `apikey`. This is
-always on and cannot be disabled.
+### Table columns
-**Advanced configuration:**
+```yaml
+tui:
+ columns:
+ - name: alias
+ width: 30 # optional, 1-200
+ - name: resource
+ - name: local
+ - name: status
+```
+
+- Valid names: `context`, `namespace`, `alias`, `type`, `resource`, `remote`, `local`, `status`.
+- List order is render order. Unlisted columns are hidden.
+- `width` is the maximum text width, 1 to 200. Without it, the column keeps its default width.
+- Without `tui`, the table keeps its default columns and order.
+- The setting applies to the UI and to the `-verbose` table, and reloads with the file.
+
+### HTTP log
+
+Press `l` on a forward that has `httpLog` set.
```yaml
forwards:
@@ -474,137 +334,98 @@ forwards:
localPort: 8080
httpLog:
enabled: true
- includeHeaders: true # values of sensitive headers are redacted
- maxBodySize: 65536 # bytes; 0 = unlimited
- filterPath: "/api/" # only log paths matching this substring
- logFile: "api.log" # append entries to a file in addition to the in-memory ring
+ includeHeaders: true # sensitive values are redacted
+ maxBodySize: 65536 # bytes, 0 = unlimited
+ filterPath: "/api/" # log only paths containing this text
+ logFile: "api.log" # also append entries to this file
```
-### Connection Benchmarking
+List view columns: TIME, METHOD, STATUS, LATENCY, PATH.
-Press `b` in the TUI to benchmark a selected forward. Configure:
+| Key | Action |
+|-----|--------|
+| `Up` `Down` | Move |
+| `Enter` | Open details |
+| `g` / `G` | Top / bottom |
+| `a` | Toggle auto-scroll |
+| `f` | Cycle filter: All, Non-2xx, Errors |
+| `/` | Search path or method |
+| `c` | Clear filters |
+| `q` | Close |
-- **URL Path** - Target endpoint (default: `/`)
-- **Method** - HTTP method (GET, POST, etc.)
-- **Concurrency** - Number of parallel workers
-- **Requests** - Total number of requests
+Detail view shows sorted request and response headers, bodies and timing. JSON bodies are pretty-printed, gzip and deflate bodies are decoded, and binary bodies show a placeholder.
-Results include:
-- Success/failure counts
-- Min/Max/Avg latency
-- P50/P95/P99 percentiles
-- Throughput (requests/sec)
-- Status code distribution
+| Key | Action |
+|-----|--------|
+| `Up` `Down` / `PgUp` `PgDn` | Scroll |
+| `g` | Top |
+| `c` | Copy response body |
+| `Esc` / `q` / `Enter` | Back to the list |
-### Hot-Reload
+With `includeHeaders: true`, kportal replaces these header values with `[REDACTED]` and keeps the name: `Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`, `X-Auth-Token`, `X-Csrf-Token`, `Proxy-Authorization`, `X-Access-Token`, and any name containing `token`, `secret`, `password` or `apikey`. This cannot be turned off.
-Configuration changes are applied automatically. Manual reload:
+### Benchmark
+
+Press `b` on a forward. Set the URL path (default `/`), HTTP method, concurrency and request count.
+
+### Reload
+
+kportal reloads when the config file changes. To reload by hand:
```bash
kill -HUP $(pgrep kportal)
```
-### Port Conflict Detection
+`Ctrl+C` and `SIGTERM` shut down cleanly.
-kportal validates port availability at startup and during hot-reload, showing which process is using conflicting ports.
-
-Conflicts are only reported between forwards that could be running together.
-With `--context`, forwards in contexts outside the selection are ignored, which
-is what lets each context reuse the same `localPort` values - see
-[Reusing local ports across contexts](#reusing-local-ports-across-contexts).
-
-### Retry Strategy
-
-Exponential backoff: 1s → 2s → 4s → 8s → 10s (max). Retries continue indefinitely until connection succeeds.
-
-## Migration from kftray
+### Import from kftray
```bash
kportal --convert configs.json --convert-output .kportal.yaml
```
-## Signal Handling
+## Troubleshooting
-- `Ctrl+C` / `SIGTERM` - Graceful shutdown
-- `SIGHUP` - Reload configuration
-
-## 🐛 Troubleshooting
-
-### Port Already in Use
+Port already in use:
```bash
lsof -i :
kill
```
-### Connection Refused
+Connection refused:
-1. Verify pod is running: `kubectl get pods -n `
-2. Verify port is correct: `kubectl describe pod `
-3. Check service endpoints: `kubectl get endpoints `
+1. Check the pod: `kubectl get pods -n `
+2. Check the port: `kubectl describe pod `
+3. Check the service endpoints: `kubectl get endpoints `
-### Context Not Found
+Context not found: list your contexts with `kubectl config get-contexts`. Names with `@`, `.`, `:` or `/` (for example `admin@home` or an EKS ARN) are valid. If `--context` reports `unknown context "..."`, the name is missing from `contexts[].name` in the config file.
+
+The add wizard shows a namespace prompt instead of a list: the cluster refused or returned an empty namespace list. Type the namespace name.
+
+## Development
+
+Requires Go 1.26 or later, kubectl and access to a cluster.
```bash
-kubectl config get-contexts
-```
-
-Context names containing `@`, `.`, `:`, or `/` (e.g. `admin@home`,
-`user@cluster.example.com`, GKE dotted names, EKS ARNs) are accepted by the
-config validator.
-
-If `--context` reports `unknown context "..."`, the name does not match any
-`contexts[].name` in the config file. The error lists the names that are
-defined - note these are the contexts in your `.kportal.yaml`, which need not be
-every context in your kubeconfig.
-
-## 🔧 Development
-
-### Prerequisites
-
-- Go 1.23+
-- Kubernetes cluster access
-- kubectl configured
-
-### Build
-
-```bash
-make build # Build binary
-make test # Run tests
+make build # build the binary
+make test # run tests
make all # fmt, vet, staticcheck, test
-make install # Install locally
+make install # install locally
```
-## Contributing
-
-See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
+See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Release history is in [CHANGELOG.md](CHANGELOG.md).
## Telemetry
-On startup this binary sends a single anonymous adoption ping — project name,
-version, timestamp; no identifiers, no command output, no payload contents.
-Fire-and-forget with a 2-second timeout; cannot block startup or panic.
+On startup kportal sends one anonymous adoption ping: project name, version and timestamp. It has a 2-second timeout and cannot block startup.
-See **[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**
-for the exact wire format, source, and full opt-out documentation.
-
-Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
-or `KPORTAL_DISABLE_TELEMETRY=1`.
+To opt out, set `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1` or `KPORTAL_DISABLE_TELEMETRY=1`. See [oss-telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry) for the wire format and source.
## License
-MIT License - see [LICENSE](LICENSE).
+MIT. See [LICENSE](LICENSE).
-## Acknowledgments
+## Credits
-- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - Terminal UI framework
-- [Lipgloss](https://github.com/charmbracelet/lipgloss) - Terminal styling
-- [client-go](https://github.com/kubernetes/client-go) - Kubernetes client
-- [kftray](https://github.com/hcavarsan/kftray) - Inspiration
-
-## Links
-
-- [Website](https://lukaszraczylo.github.io/kportal)
-- [Issues](https://github.com/lukaszraczylo/kportal/issues)
-- [Releases](https://github.com/lukaszraczylo/kportal/releases)
-- [Changelog](CHANGELOG.md)
+[Bubble Tea](https://github.com/charmbracelet/bubbletea), [Lipgloss](https://github.com/charmbracelet/lipgloss), [client-go](https://github.com/kubernetes/client-go). [kftray](https://github.com/hcavarsan/kftray) was the inspiration.