diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..ba92d34
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,69 @@
+name: Docs
+
+on: push
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+
+ - name: Install tools
+ run: |
+ go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0
+ go install github.com/gohugoio/hugo@v0.161.1
+
+ - name: Generate API reference
+ run: |
+ mkdir -p docs/content/docs/reference
+ gomarkdoc --output docs/content/docs/reference/config.md ./internal/config/
+ gomarkdoc --output docs/content/docs/reference/db.md ./internal/db/
+ gomarkdoc --output docs/content/docs/reference/rules.md ./internal/rules/
+ gomarkdoc --output docs/content/docs/reference/tui.md ./internal/tui/
+ gomarkdoc --output docs/content/docs/reference/theme.md ./internal/tui/theme/
+ gomarkdoc --output docs/content/docs/reference/views.md ./internal/tui/views/
+
+ - name: Add front matter to generated docs
+ run: |
+ for f in docs/content/docs/reference/*.md; do
+ name=$(basename "$f" .md)
+ if [ "$name" = "_index" ]; then continue; fi
+ tmp=$(mktemp)
+ printf -- '---\ntitle: %s\n---\n\n' "$name" > "$tmp"
+ cat "$f" >> "$tmp"
+ mv "$tmp" "$f"
+ done
+
+ - name: Build site
+ run: cd docs && hugo --minify
+
+ - name: Upload artifact
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
+ with:
+ path: docs/public/
+
+ deploy:
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
diff --git a/.gitignore b/.gitignore
index 1274ebd..70e61b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,19 @@ dist/
# env file
.env
+# Hugo
+docs/public/
+docs/resources/
+docs/.hugo_build.lock
+
+# Generated API reference docs
+docs/content/docs/reference/config.md
+docs/content/docs/reference/db.md
+docs/content/docs/reference/rules.md
+docs/content/docs/reference/tui.md
+docs/content/docs/reference/theme.md
+docs/content/docs/reference/views.md
+
# Editor/IDE
# .idea/
# .vscode/
diff --git a/README.md b/README.md
index bd51746..5a94382 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+[](https://github.com/Fraser-Isbester/tusk/actions/workflows/ci.yml)
+[](https://goreportcard.com/report/github.com/fraser-isbester/tusk)
+
# Tusk
A k9s-style terminal UI for real-time PostgreSQL monitoring and management. Not a database client — a tool for observing and acting on live database activity.
diff --git a/docs/content/_index.md b/docs/content/_index.md
new file mode 100644
index 0000000..eac009a
--- /dev/null
+++ b/docs/content/_index.md
@@ -0,0 +1,28 @@
+---
+title: Tusk
+layout: hextra-home
+---
+
+
+{{< hextra/hero-headline >}}
+ Tusk
+{{< /hextra/hero-headline >}}
+
+
+
+{{< hextra/hero-subtitle >}}
+ A k9s-style terminal UI for real-time PostgreSQL monitoring and management
+{{< /hextra/hero-subtitle >}}
+
+
+
+{{< hextra/hero-button text="Get Started" link="docs/getting-started" >}}
+
+
+## Features
+
+{{< cards >}}
+ {{< card link="docs/views" title="Live Views" subtitle="Queries, transactions, sessions, locks, tables, indexes with 2s auto-refresh" >}}
+ {{< card link="docs/rules" title="Rules Engine" subtitle="CEL-based policy rules evaluated against live database state" >}}
+ {{< card link="docs/keybindings" title="Keyboard-Driven" subtitle="k9s-inspired navigation with column sorting, filtering, and drill-down" >}}
+{{< /cards >}}
diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md
new file mode 100644
index 0000000..1b1eaec
--- /dev/null
+++ b/docs/content/docs/_index.md
@@ -0,0 +1,8 @@
+---
+title: Documentation
+next: getting-started
+sidebar:
+ open: true
+---
+
+Tusk is a terminal UI for real-time PostgreSQL monitoring. It connects to live Postgres instances and provides interactive views of database activity, with a rules engine for policy enforcement.
diff --git a/docs/content/docs/development.md b/docs/content/docs/development.md
new file mode 100644
index 0000000..c63b2bd
--- /dev/null
+++ b/docs/content/docs/development.md
@@ -0,0 +1,59 @@
+---
+title: Development
+weight: 5
+---
+
+## Architecture
+
+```
+cmd/tusk/main.go Entry point, CLI flags, profile resolution, engine setup
+internal/config/ YAML config with profiles, connection strings, rule definitions
+internal/db/ PostgreSQL connectivity (pgx pool), query methods, data types
+internal/rules/ CEL rules engine, violation store, action executors
+internal/tui/ TUI application (tview), views, detail panes, navigation
+internal/tui/theme/ Color palette and styles
+internal/tui/views/ Individual view implementations (queries, transactions, etc.)
+scripts/ Load testing and seed data
+```
+
+## Key types
+
+- `db.ResourceBase` -- shared fields for all `pg_stat_activity` resources (PID, User, App, Database, State, BackendStart)
+- `db.Query` -- active query with duration, wait events, SQLcommenter tags, BlockedBy, QueryID, QueryStart
+- `db.Transaction` -- active transaction with XactStart, XactDuration, QueryDuration, LockCount
+- `rules.Violation` -- rule violation with timestamped event log (detected, action, cooldown, closed)
+- `rules.Engine` -- evaluates CEL rules against snapshots every 2s, manages violation store
+
+## Conventions
+
+- Views implement the `tui.View` interface: `Table()`, `Start()`, `Stop()`, `ItemCount()`, `SetFilter()`
+- Detail views return `*tview.Flex` with split panes (Info, Query/Queries, Activity)
+- Navigation uses a view stack with `Esc` to go back; `Navigator` callback for drill-down
+- `Tab` / `Shift+Tab` cycles focus between panes in detail views
+- All table views save/restore selection across refresh cycles
+- `Stop()` methods guard against double-close with `v.done = nil` after close
+- Query identity: `(PID, BackendStart)` for sessions, `(PID, XactStart)` for transactions
+- SQL formatting is display-only via `formatSQL()` before `highlightSQL()`
+
+## Build and test
+
+```bash
+task build # go build -o tusk ./cmd/tusk
+task run # build + run with dev profile
+task db:up # start local Postgres in Docker
+task loadtest # generate realistic load
+task test # run all tests with Ginkgo
+task check # run vet + fmt check + tests
+./tusk -P # run with a named profile
+./tusk '' # run with a direct connection string
+```
+
+## Dependencies
+
+| Package | Purpose |
+|---------|---------|
+| `rivo/tview` + `gdamore/tcell` | TUI framework |
+| `jackc/pgx/v5` | PostgreSQL driver and connection pool |
+| `google/cel-go` | Common Expression Language for rule evaluation |
+| `spf13/cobra` | CLI framework |
+| `gopkg.in/yaml.v3` | Configuration parsing |
diff --git a/docs/content/docs/getting-started.md b/docs/content/docs/getting-started.md
new file mode 100644
index 0000000..401a380
--- /dev/null
+++ b/docs/content/docs/getting-started.md
@@ -0,0 +1,98 @@
+---
+title: Getting Started
+weight: 1
+---
+
+## Install
+
+### From source
+
+```bash
+go install github.com/fraser-isbester/tusk/cmd/tusk@latest
+```
+
+### Pre-built binaries
+
+Download from [GitHub Releases](https://github.com/fraser-isbester/tusk/releases).
+
+## Usage
+
+```bash
+# Direct connection
+tusk 'postgres://user:pass@localhost:5432/mydb'
+
+# Using a profile
+tusk -P production
+```
+
+## Configuration
+
+Tusk reads its configuration from `~/.config/tusk/config.yaml`. A configuration file defines one or more connection profiles, each with optional rules.
+
+```yaml
+default_profile: dev
+
+profiles:
+ dev:
+ url: "postgres://postgres:postgres@localhost:5432/mydb?sslmode=disable"
+ rules:
+ - name: kill-idle-in-txn
+ resource: transaction
+ when: "state == 'idle in transaction' && xact_duration > duration('5m')"
+ action: terminate
+ cooldown: 5m
+ dry_run: true
+
+ - name: long-queries
+ resource: query
+ when: "state == 'active' && duration > duration('30s')"
+ action: cancel
+ cooldown: 1m
+ dry_run: true
+
+ production:
+ host: db.example.com
+ port: 5432
+ user: monitor
+ password: 'secret'
+ database: prod
+ readonly: true
+ rules:
+ - name: idle-in-txn-5m
+ resource: transaction
+ when: "state == 'idle in transaction' && xact_duration > duration('5m')"
+ action: terminate
+ cooldown: 5m
+ dry_run: true
+```
+
+### Profile fields
+
+Profiles can specify a connection either as a full `url` or with individual fields (`host`, `port`, `user`, `password`, `database`, `sslmode`). Additional profile-level options:
+
+| Field | Description |
+|-------|-------------|
+| `readonly` | Forces all rules in this profile to dry-run mode |
+| `color` | Profile accent color for the header |
+| `refresh_interval` | Polling interval (default `2s`) |
+
+### Rule fields
+
+| Field | Description |
+|-------|-------------|
+| `name` | Human-readable identifier for the rule |
+| `resource` | `query`, `transaction`, or `lock` |
+| `when` | CEL expression evaluated against the resource fields |
+| `action` | `terminate`, `cancel`, or `log` |
+| `cooldown` | Minimum interval between action firings per PID |
+| `dry_run` | Record violations but don't execute the action |
+
+### CEL expression fields
+
+Each resource type exposes different fields to CEL expressions:
+
+**Query**: `pid`, `user`, `app`, `database`, `state`, `duration`, `wait_event_type`, `wait_event`, `query`, `blocked_by`, `query_id`, `route`, `controller`, `action_name`, `framework`
+
+**Transaction**: `pid`, `user`, `app`, `database`, `state`, `xact_duration`, `query_duration`, `query`, `lock_count`
+
+**Lock**: `blocked_pid`, `blocking_pid`, `blocked_user`, `blocking_user`, `blocked_app`, `blocking_app`, `lock_type`, `mode`, `wait_duration`
diff --git a/docs/content/docs/keybindings.md b/docs/content/docs/keybindings.md
new file mode 100644
index 0000000..89890fe
--- /dev/null
+++ b/docs/content/docs/keybindings.md
@@ -0,0 +1,58 @@
+---
+title: Key Bindings
+weight: 4
+---
+
+Tusk uses keyboard-driven navigation inspired by k9s and vim.
+
+## Global keys
+
+| Key | Action |
+|-----|--------|
+| `Left` / `Right` | Switch between views |
+| `:` | Open command prompt (type a view name, e.g. `:queries`) |
+| `/` | Open filter input |
+| `h` | Show help overlay |
+| `q` | Quit |
+| `Ctrl+C` | Quit |
+
+## Table views
+
+| Key | Action |
+|-----|--------|
+| `Enter` | Drill into detail view for the selected row |
+| `Esc` | Go back to the previous view / clear filter |
+| `Shift+letter` | Sort by column (e.g. `Shift+D` for duration) |
+
+## Query and transaction views
+
+| Key | Action |
+|-----|--------|
+| `c` | Cancel query (`pg_cancel_backend`) |
+| `t` | Terminate backend (`pg_terminate_backend`) |
+
+## Lock view
+
+| Key | Action |
+|-----|--------|
+| `t` | Terminate the blocking backend |
+
+## Detail views
+
+| Key | Action |
+|-----|--------|
+| `Tab` | Cycle focus to the next pane |
+| `Shift+Tab` | Cycle focus to the previous pane |
+| `Esc` | Return to the parent list view |
+| `c` | Cancel query (in query/transaction detail) |
+| `t` | Terminate backend (in query/transaction detail) |
+
+## Filter
+
+| Key | Action |
+|-----|--------|
+| `/` | Open filter input |
+| `Enter` | Apply filter and return focus to the table |
+| `Esc` | Clear filter and close filter input |
+
+Filter text is matched case-insensitively across all visible columns. The filter persists until cleared with `Esc`.
diff --git a/docs/content/docs/reference/_index.md b/docs/content/docs/reference/_index.md
new file mode 100644
index 0000000..84694b1
--- /dev/null
+++ b/docs/content/docs/reference/_index.md
@@ -0,0 +1,6 @@
+---
+title: API Reference
+weight: 10
+---
+
+These pages are auto-generated from Go source using [gomarkdoc](https://github.com/princjef/gomarkdoc). They are rebuilt on every push by the docs CI workflow and should not be edited by hand.
diff --git a/docs/content/docs/rules.md b/docs/content/docs/rules.md
new file mode 100644
index 0000000..7cc1932
--- /dev/null
+++ b/docs/content/docs/rules.md
@@ -0,0 +1,73 @@
+---
+title: Rules Engine
+weight: 3
+---
+
+Tusk includes a CEL-based rules engine that evaluates policy rules against live database state every 2 seconds. Rules are defined per-profile in the YAML configuration.
+
+## Rule fields
+
+| Field | Description |
+|-------|-------------|
+| `name` | Human-readable identifier for the rule |
+| `resource` | `query`, `transaction`, or `lock` |
+| `when` | CEL expression evaluated against the resource fields |
+| `action` | `terminate`, `cancel`, or `log` |
+| `cooldown` | Minimum interval between action firings per PID |
+| `dry_run` | Record violations but don't execute the action |
+
+## CEL expression fields
+
+Each resource type exposes a different set of fields to CEL expressions.
+
+### Query
+
+`pid`, `user`, `app`, `database`, `state`, `duration`, `wait_event_type`, `wait_event`, `query`, `blocked_by`, `query_id`, `route`, `controller`, `action_name`, `framework`
+
+### Transaction
+
+`pid`, `user`, `app`, `database`, `state`, `xact_duration`, `query_duration`, `query`, `lock_count`
+
+### Lock
+
+`blocked_pid`, `blocking_pid`, `blocked_user`, `blocking_user`, `blocked_app`, `blocking_app`, `lock_type`, `mode`, `wait_duration`
+
+## Example rules
+
+```yaml
+rules:
+ - name: kill-idle-in-txn
+ resource: transaction
+ when: "state == 'idle in transaction' && xact_duration > duration('5m')"
+ action: terminate
+ cooldown: 5m
+ dry_run: true
+
+ - name: long-queries
+ resource: query
+ when: "state == 'active' && duration > duration('30s')"
+ action: cancel
+ cooldown: 1m
+ dry_run: true
+```
+
+## Violation lifecycle
+
+When a rule's CEL expression matches a resource, a violation is recorded and progresses through a lifecycle:
+
+1. **Detected** -- The rule expression matched a resource. A violation is created with a timestamped event.
+2. **Action** -- The configured action is triggered (or logged if `dry_run: true`).
+3. **Cooldown** -- If a `cooldown` is set, no further actions fire for that PID until the cooldown expires. The violation remains visible.
+4. **Closed** -- When the PID is no longer present in the database snapshot, the violation is marked as closed.
+
+Violations are visible in the `:violations` view with their full event timeline.
+
+## Dry-run and copilot mode
+
+All rules default to `dry_run: true`. In this mode, violations are recorded and displayed but no action is executed against the database. This allows you to observe what would happen before enabling automatic actions.
+
+From the Activity pane in query or transaction detail views, you can manually fire an action by pressing `Enter` on a violation row. This "copilot" workflow lets you review each violation before taking action.
+
+## Readonly profiles
+
+Setting `readonly: true` at the profile level forces all rules in that profile to dry-run mode, regardless of their individual `dry_run` setting. This is useful for monitoring profiles that should never modify the database.
diff --git a/docs/content/docs/views.md b/docs/content/docs/views.md
new file mode 100644
index 0000000..d0573b2
--- /dev/null
+++ b/docs/content/docs/views.md
@@ -0,0 +1,45 @@
+---
+title: Views
+weight: 2
+---
+
+Tusk provides eight resource views, each accessible via the command prompt (`:`) or arrow-key navigation.
+
+## Resource views
+
+| Command | Description |
+|---------|-------------|
+| `:queries` | Active queries with duration, wait events, blocking info, and rule violation indicators |
+| `:transactions` | Active transactions sorted by age with lock counts and transaction duration |
+| `:sessions` | Connections grouped by user, application, and state |
+| `:tables` | Table sizes, row counts, dead tuple percentages, and vacuum statistics |
+| `:locks` | Blocked/blocking lock pairs with wait duration and lock type |
+| `:indexes` | Index scan counts, sizes, and usage statistics |
+| `:rules` | Configured rules with violation counts and current status |
+| `:violations` | Violation audit log with timestamped event timeline |
+
+All views auto-refresh every 2 seconds. The active view is highlighted in the tab bar, and the item count is shown in parentheses.
+
+## Detail views
+
+Pressing `Enter` on a row in most views opens a detail view with split panes showing additional information.
+
+### Query detail
+
+Shows formatted and syntax-highlighted SQL, query metadata (PID, user, app, database, state, duration, wait events), and an Activity pane displaying lock contention and rule violations for the query's PID. Press `c` to cancel the query or `t` to terminate the backend.
+
+### Transaction detail
+
+Displays transaction metadata (PID, user, state, transaction duration, query duration, lock count) alongside the current query text. Includes a query history pane showing previous queries executed within the same transaction, and an Activity pane with violations.
+
+### Lock detail
+
+Shows the blocked and blocking backends side-by-side with their respective queries, users, applications, and lock information (lock type, mode, wait duration).
+
+### Table detail
+
+Displays detailed table statistics including size, row estimates, dead tuples, sequential vs. index scan ratios, and vacuum/analyze timestamps.
+
+## Pane navigation
+
+Detail views contain multiple panes. Use `Tab` to move focus to the next pane and `Shift+Tab` to move to the previous pane. The currently focused pane is highlighted with a brighter border.
diff --git a/docs/go.mod b/docs/go.mod
new file mode 100644
index 0000000..7bdc745
--- /dev/null
+++ b/docs/go.mod
@@ -0,0 +1,5 @@
+module github.com/fraser-isbester/tusk/docs
+
+go 1.26.2
+
+require github.com/imfing/hextra v0.12.2 // indirect
diff --git a/docs/go.sum b/docs/go.sum
new file mode 100644
index 0000000..583c3df
--- /dev/null
+++ b/docs/go.sum
@@ -0,0 +1,2 @@
+github.com/imfing/hextra v0.12.2 h1:qa+cHQ1LC/7ys9EhRNnHrRBAHu83Tm8rhh1oWO2a7cc=
+github.com/imfing/hextra v0.12.2/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo=
diff --git a/docs/hugo.yaml b/docs/hugo.yaml
new file mode 100644
index 0000000..615676e
--- /dev/null
+++ b/docs/hugo.yaml
@@ -0,0 +1,29 @@
+baseURL: https://fraser-isbester.github.io/tusk/
+title: Tusk
+languageCode: en-us
+
+module:
+ imports:
+ - path: github.com/imfing/hextra
+
+menu:
+ main:
+ - name: Docs
+ pageRef: /docs
+ weight: 1
+ - name: GitHub
+ url: https://github.com/Fraser-Isbester/tusk
+ weight: 2
+ params:
+ icon: github
+
+params:
+ description: A k9s-style terminal UI for PostgreSQL monitoring
+ navbar:
+ displayTitle: true
+ displayLogo: false
+ footer:
+ displayPoweredBy: false
+ editURL:
+ enable: true
+ base: https://github.com/Fraser-Isbester/tusk/edit/main/docs/content
diff --git a/internal/config/config.go b/internal/config/config.go
index 7382643..d68c9dd 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,3 +1,4 @@
+// Package config handles YAML profile loading and connection string resolution.
package config
import (
diff --git a/internal/db/pool.go b/internal/db/pool.go
index 341c0dc..3e6613a 100644
--- a/internal/db/pool.go
+++ b/internal/db/pool.go
@@ -1,3 +1,4 @@
+// Package db provides PostgreSQL connectivity, query execution, and data types.
package db
import (
diff --git a/internal/rules/engine.go b/internal/rules/engine.go
index 0d0d7d1..70f00da 100644
--- a/internal/rules/engine.go
+++ b/internal/rules/engine.go
@@ -1,3 +1,4 @@
+// Package rules implements a CEL-based policy engine with violation tracking and action execution.
package rules
import (
diff --git a/internal/tui/app.go b/internal/tui/app.go
index 632d738..826ebea 100644
--- a/internal/tui/app.go
+++ b/internal/tui/app.go
@@ -1,3 +1,4 @@
+// Package tui is the root terminal UI application built on tview.
package tui
import (
@@ -375,15 +376,16 @@ func (a *App) updateHeader() {
y := "[#FFD700]"
r := "[#FF5F5F]"
- // TUSK in big open letters (figlet "big" style)
- o := "[#D78700]"
logo := []string{
- o + tview.Escape(" _______ _ _ _____ _ __") + "[-]",
- o + tview.Escape(" |__ __|| | | | / ____|| |/ /") + "[-]",
- o + tview.Escape(" | | | | | || (___ | ' / ") + "[-]",
- o + tview.Escape(" | | | | | | \\___ \\ | < ") + "[-]",
- o + tview.Escape(" | | | |__| | ____) || . \\ ") + "[-]",
- o + tview.Escape(" |_| \\____/ |_____/ |_|\\_\\") + "[-]",
+ "",
+ "",
+ "",
+ c + ` _______ _ _ _______ _ _` + "[-]",
+ c + ` | | | |______ |____/ ` + "[-]",
+ c + ` | |_____| ______| | \_` + "[-]",
+ "",
+ "",
+ "",
}
uptimeStr := "--"
@@ -436,7 +438,7 @@ func (a *App) updateHeader() {
if headerWidth <= 0 {
headerWidth = 120
}
- logoWidth := 33 // visual width of the big letters
+ logoWidth := 34 // visual width of cyberlarge logo
var lines []string
for i := 0; i < len(infoLines) || i < len(logo); i++ {
diff --git a/internal/tui/theme/theme.go b/internal/tui/theme/theme.go
index 1bd7674..c472d42 100644
--- a/internal/tui/theme/theme.go
+++ b/internal/tui/theme/theme.go
@@ -1,3 +1,4 @@
+// Package theme defines the color palette and styles for the TUI.
package theme
import (
diff --git a/internal/tui/views/queries.go b/internal/tui/views/queries.go
index 924c861..a783f05 100644
--- a/internal/tui/views/queries.go
+++ b/internal/tui/views/queries.go
@@ -1,3 +1,4 @@
+// Package views implements the individual resource views for the TUI.
package views
import (