middleman

package module
v0.0.0-...-17c6387 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 12, 2026 License: MIT Imports: 16 Imported by: 0

README

middleman

A local-first GitHub dashboard for project maintainers. Syncs PRs and issues from your repos into SQLite, serves a fast Svelte 5 frontend from a single binary, and keeps you out of GitHub's notification inbox.

Middleman runs entirely on your machine -- no hosted service, no telemetry, no account to create. One binary, one config file, and you're up.

Features

Activity feed

A unified timeline of comments, reviews, and commits across all your repos. Switch between flat and threaded views. Threaded view groups events by PR/issue and collapses long commit runs for readability.

Filter by time range (24h / 7d / 30d / 90d), event type, repo, item type (PRs vs issues), or free-text search. Hide closed items and bot noise with a toggle.

Pull request management

Browse, search, and filter PRs across repos. Group by repo or show a flat list. From the detail view you can:

  • Comment directly on a PR
  • Approve a PR
  • Merge with your choice of merge commit, squash, or rebase
  • Mark draft PRs as ready for review
  • Close and reopen PRs
  • Star items for quick filtering

Review decisions, diff stats (additions/deletions), CI status, merge conflict indicators, and branch info are visible at a glance.

Diff view

Inline diffs with a collapsible file tree sidebar. Files are grouped by directory and show status badges (modified, added, deleted, renamed) with per-file addition/deletion counts. Syntax highlighting via Shiki with light/dark theme support.

Filter the file tree by name, toggle whitespace visibility, and adjust tab width. Navigate between files with j/k. Each file section is independently collapsible.

Kanban board

Track PRs through New / Reviewing / Waiting / Awaiting Merge columns with drag-and-drop. Kanban state is local to middleman -- it doesn't touch your GitHub labels or projects.

Issue tracking

Same filtering, search, and detail view as PRs. Post comments, close/reopen, and star issues without context-switching to GitHub.

CI checks

Expandable check run section on each PR shows pass/fail/pending status with color-coded indicators and direct links to the failing run on GitHub.

Sync engine
  • Runs immediately on startup, then on a configurable interval (default 5 minutes)
  • Opening a PR or issue triggers an immediate sync for that item
  • The active detail view polls every 60 seconds for new comments
  • Progress is visible in the status bar; errors surface clearly
Keyboard navigation
Key Action
j / k Move through the list (or between files in diff view)
1 / 2 Switch between list and kanban views
Escape Close detail view / clear selection
Other
  • Dark mode -- auto-detects system preference, with a manual toggle
  • GitHub Enterprise -- set platform_host per repo to connect to GHE instances
  • Copy to clipboard -- one-click copy of PR/issue bodies and comments
  • Settings UI -- add/remove repos and configure activity feed defaults from the browser
  • Reverse proxy support -- deploy behind a proxy with the base_path config
  • Version info -- middleman version prints the version, commit, and build date

Quickstart

Requirements
  • Go 1.26+
  • Bun (or install via mise)
  • A GitHub token (classic or fine-grained with repo read access)
Build and run
git clone https://github.com/wesm/middleman.git
cd middleman
make build

Set your token and start middleman:

export MIDDLEMAN_GITHUB_TOKEN=ghp_your_token_here
./middleman

If you use the GitHub CLI, middleman will use gh auth token automatically -- no env var needed.

On first run, middleman creates a default config at ~/.config/middleman/config.toml and serves the UI at http://localhost:8090. Add repositories from the Settings page, or edit the config file directly:

[[repos]]
owner = "your-org"
name = "your-repo"

[[repos]]
owner = "your-org"
name = "another-repo"
Install to PATH
make install   # installs to ~/.local/bin

Configuration

All fields are optional. Repos can be added in the config file or through the Settings UI.

Field Default Description
sync_interval "5m" How often to pull from GitHub
github_token_env "MIDDLEMAN_GITHUB_TOKEN" Env var holding your token
host "127.0.0.1" Listen address
port 8090 Listen port
base_path "/" URL prefix for reverse proxy deployments
data_dir "~/.config/middleman" Directory for the SQLite database
activity.view_mode "threaded" "flat" or "threaded"
activity.time_range "7d" "24h", "7d", "30d", or "90d"
activity.hide_closed false Hide closed/merged items in the feed
activity.hide_bots false Hide bot activity
GitHub Enterprise

Add platform_host and optionally token_env to repos hosted on a GHE instance:

[[repos]]
owner = "team"
name = "internal-app"
platform_host = "github.corp.example.com"
token_env = "GHE_TOKEN"

Each distinct host can use a separate token env var. Repos without platform_host default to github.com.

Embedding

Middleman can be embedded as a Go library inside another application. The host creates an Instance, which provides an http.Handler for the API and frontend:

inst, err := middleman.New(middleman.Options{
    Token:        os.Getenv("GITHUB_TOKEN"),
    DBPath:       "/path/to/middleman.db",
    BasePath:     "/middleman/",
    SyncInterval: 5 * time.Minute,
    Repos: []middleman.Repo{
        {Owner: "org", Name: "repo"},
    },
})
if err != nil {
    log.Fatal(err)
}
defer inst.Close()
inst.StartSync(ctx)

mux.Handle("/middleman/", inst.Handler())

The EmbedConfig option controls theming (light/dark mode, custom colors, fonts, radii) and UI defaults (hide sync controls, pin to a single repo, collapse sidebar). The EmbedHooks option provides lifecycle callbacks (OnMRSynced, OnSyncCompleted) so the host can react to sync events.

The frontend is also available as the @middleman/ui Svelte package, which exports individual views (PRListView, KanbanBoardView, ActivityFeedView), store factories, and context accessors. The @middleman/ui Provider component accepts an action registry for injecting custom buttons into PR and issue detail views.

Architecture

Middleman is a single Go binary with the Svelte frontend embedded at build time. No external services -- just SQLite on disk.

middleman binary
  |- Config loader (TOML)
  |- Sync engine -> GitHub API (go-github)
  |- SQLite database (WAL mode, pure Go driver)
  +- HTTP server (Huma) -> REST API + embedded SPA
  • No CGO required -- uses modernc.org/sqlite, a pure Go SQLite implementation
  • Loopback only -- binds to 127.0.0.1 by default; this is a personal tool, not a shared service
  • Graceful shutdown -- handles SIGINT/SIGTERM cleanly

Database

Middleman uses SQLite with embedded SQL migrations in internal/db/migrations/, applied on startup via github.com/golang-migrate/migrate/v4.

On startup:

  • Fresh database: all embedded migrations are applied.
  • Legacy database without schema_migrations: middleman assumes the pre-migration schema is baseline version 1 and migrates forward.
  • Dirty or failed migration state: startup fails and instructs you to delete the database file and let middleman recreate it.
  • Newer database (migration version > binary): startup fails and instructs you to upgrade middleman.

If a migration cannot be applied cleanly, delete ~/.config/middleman/middleman.db and let middleman recreate it. Sync data will be repopulated from GitHub on the next run; local-only state (kanban columns, stars, and worktree links) is lost.

Development

Run the Go backend and Vite dev server in parallel:

make air-install    # one-time: install air for live reload
make dev            # Go server on :8090 with live reload
make frontend-dev   # Vite on :5173, proxies /api to Go

Other targets:

make build          # Debug build with embedded frontend
make build-release  # Optimized, stripped release binary
make test           # All Go tests
make test-short     # Fast tests only
make lint           # golangci-lint
make frontend-check # Svelte and TypeScript checks
make api-generate   # Regenerate OpenAPI spec and clients
make clean          # Remove build artifacts
Pre-commit hooks

Managed with prek:

brew install prek
prek install

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Activity

type Activity struct {
	ViewMode   string
	TimeRange  string
	HideClosed bool
	HideBots   bool
}

Activity configures the activity view defaults.

type EmbedConfig

type EmbedConfig = server.EmbedConfig

Type aliases so external callers don't need to import internal packages.

type EmbedHooks

type EmbedHooks struct {
	OnMRSynced      func(MergeRequestSummary)
	OnSyncCompleted func(results []RepoSyncResult)
}

EmbedHooks provides lifecycle callbacks for embedded consumers.

Concurrency:

  • OnMRSynced fires after each merge request is synced. Sync processes repos in parallel, so this callback may be invoked from multiple goroutines concurrently (one per in-flight repo sync). Implementations must be safe for concurrent use and must not block indefinitely or they will stall sync progress.
  • OnSyncCompleted fires once at the end of each sync pass on the goroutine that drives the sync, so it is not invoked concurrently with itself.

Hooks should be set before calling StartSync. Mutating the hook fields while a sync is in flight is not safe.

type Instance

type Instance struct {
	// contains filtered or unexported fields
}

Instance holds a running middleman server and its resources.

func New

func New(opts Options) (*Instance, error)

New creates a middleman Instance from the given options. Either Token or ResolveToken must yield a non-empty token. Either DBPath or DataDir must be provided.

func (*Instance) Close

func (i *Instance) Close() error

Close stops sync and closes the database. Safe to call multiple times.

func (*Instance) Handler

func (i *Instance) Handler() http.Handler

Handler returns the HTTP handler for this instance.

func (*Instance) PurgeOtherHosts

func (inst *Instance) PurgeOtherHosts(keepHost string) error

PurgeOtherHosts deletes all data for platform hosts other than keepHost.

func (*Instance) SetActiveWorktree

func (inst *Instance) SetActiveWorktree(key string)

SetActiveWorktree sets the key of the currently focused worktree. This is bootstrap-only state: it is injected into the initial HTML page load. An already-loaded SPA will not see the change until the next full page load. For live updates, mutate window.__middleman_config.ui.activeWorktreeKey in the browser and call window.__middleman_notify_config_changed().

func (*Instance) SetWatchedMRs

func (inst *Instance) SetWatchedMRs(mrs []WatchedMR)

SetWatchedMRs sets the list of merge requests to sync on a fast interval. Replaces any previous watch list.

func (inst *Instance) SetWorktreeLinks(
	links []WorktreeLink,
) error

SetWorktreeLinks replaces all worktree links atomically.

func (*Instance) StartSync

func (i *Instance) StartSync(ctx context.Context)

StartSync begins periodic GitHub sync in the background. The context is used for cancellation during Close.

StartSync must be called at most once per Instance. Once StopSync (or Close) has stopped the syncer, the underlying Syncer cannot be restarted — a subsequent StartSync call is a silent no-op. Construct a new Instance if sync must run again.

func (*Instance) StopSync

func (i *Instance) StopSync()

StopSync stops the periodic GitHub sync. This operation is terminal: the underlying Syncer permanently refuses further Start or TriggerRun calls after Stop, so callers that need to resume sync must create a new Instance.

Safe to call concurrently. The cancelHook check/call/reset is protected by cancelHookOnce so only the first caller cancels the stack-detection context. i.syncer.Stop() runs on every call by design: Syncer.Stop waits up to stopGracePeriod on each call, so a Close() following a StopSync() that hit the grace-period timeout can still re-wait for lingering work rather than closing the database out from under it.

type MergeRequestSummary

type MergeRequestSummary struct {
	MergeRequestID int64
	RepoOwner      string
	RepoName       string
	Number         int
	State          string
	Title          string
	IsDraft        bool
	CIStatus       string
	ReviewDecision string
	PlatformHost   string
	CIChecksJSON   string
	UpdatedAt      time.Time
}

MergeRequestSummary is a lightweight snapshot of a synced MR, passed to EmbedHooks callbacks.

type Options

type Options struct {
	// Token is a static GitHub token. Used when ResolveToken
	// is nil.
	Token string
	// ResolveToken returns a GitHub token for the given platform
	// host (e.g. "github.com"). Preferred over Token for
	// embedded use cases that need per-host auth.
	ResolveToken func(ctx context.Context, host string) (string, error)
	// DataDir is the directory for middleman state. Required if
	// DBPath is not set.
	DataDir string
	// DBPath overrides the DataDir-derived database path. When
	// set, the host owns the SQLite file and DataDir may be
	// omitted.
	DBPath        string
	BasePath      string
	SyncInterval  time.Duration
	WatchInterval time.Duration
	Repos         []Repo
	Activity      Activity
	Assets        fs.FS
	EmbedConfig   *server.EmbedConfig
	EmbedHooks    *EmbedHooks
}

Options configures a middleman Instance for embedding.

type Repo

type Repo struct {
	Owner        string
	Name         string
	PlatformHost string // e.g. "github.com" or GHE hostname
}

Repo identifies a GitHub repository to monitor.

type RepoRef

type RepoRef = server.RepoRef

Type aliases so external callers don't need to import internal packages.

type RepoSyncResult

type RepoSyncResult struct {
	Owner        string
	Name         string
	PlatformHost string
	Error        string // empty on success
}

RepoSyncResult holds the outcome of syncing a single repo.

type ThemeConfig

type ThemeConfig = server.ThemeConfig

Type aliases so external callers don't need to import internal packages.

type UIConfig

type UIConfig = server.UIConfig

Type aliases so external callers don't need to import internal packages.

type WatchedMR

type WatchedMR = ghclient.WatchedMR

Type aliases so external callers don't need to import internal packages.

type WorktreeLink = db.WorktreeLink

Type aliases so external callers don't need to import internal packages.

Directories

Path Synopsis
cmd
e2e-server command
middleman command
internal
apiclient/generated
Package generated provides primitives to interact with the openapi HTTP API.
Package generated provides primitives to interact with the openapi HTTP API.
db
web
tools

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL