Skip to content

Latest commit

 

History

History
378 lines (291 loc) · 13.1 KB

File metadata and controls

378 lines (291 loc) · 13.1 KB

synapse-admin — Agent & Developer Guide

Terminal-based admin TUI for Matrix Synapse homeservers. Single static binary, no runtime dependencies, communicates directly with the Synapse Admin API over HTTPS.


Quick reference

go build .                        # build binary
go run .                          # run from source
go test ./...                     # run all tests
go vet ./...                      # static analysis
gofumpt -w .                      # format (stricter than gofmt)
golangci-lint run                 # lint

Primary config file path: ~/.synapse-admin/config.toml

Legacy fallback read path: ~/Library/Application Support/synapse-admin/config.toml


Project layout

synapse-admin/
├── main.go                          # entry point: load config → build client → start tea.Program
├── go.mod
├── go.sum
├── README.md
├── AGENTS.md                        # this file
└── internal/
    ├── api/
    │   ├── client.go                # HTTP client, auth header, error unwrapping
    │   ├── json.go                  # tolerant numeric/string JSON decoding helpers
    │   ├── users.go                 # /v2/users endpoints + activate/deactivate helpers
    │   ├── rooms.go                 # /v1/rooms endpoints
    │   └── tokens.go                # /v1/registration_tokens endpoints
    ├── config/
    │   └── config.go                # TOML Config struct, Load(), Save(), ConfigPath()
    └── ui/
        ├── model/
        │   ├── root.go              # RootModel: routing, modal orchestration, status bar
        │   ├── keys.go              # all key.Binding definitions
        │   └── statusbar.go         # renderStatusBar()
        ├── views/
        │   ├── actions.go           # typed messages from views to root for mutations
        │   ├── common.go            # shared filter + empty-state helpers
        │   ├── errors.go            # shared view errors
        │   ├── helpers.go           # small rendering utilities
        │   ├── users.go             # UsersView — table + API search + local prev history
        │   ├── rooms.go             # RoomsView — table + pagination + search
        │   └── tokens.go            # TokensView — registration token table
        ├── dialog/
        │   ├── dialog.go            # Dialog interface + shared message types
        │   ├── command_palette.go   # ctrl+p command palette
        │   ├── config_dialog.go     # homeserver / access-token form
        │   └── confirm_dialog.go    # generic confirmation overlay
        └── styles/
            └── styles.go            # all lipgloss styles, adaptive light/dark colors

Rule: only internal/ui/model/root.go implements the full Bubble Tea Update(tea.Msg) cycle. Every other UI struct exposes plain methods (SetSize, SetClient, Load, Update, View) that return tea.Cmd. This is the "smart root, dumb views" pattern. Keep it.


Architecture

Tech stack

Layer Package Notes
TUI framework github.com/charmbracelet/bubbletea v1 Elm architecture
Styling github.com/charmbracelet/lipgloss adaptive colors
Input components github.com/charmbracelet/bubbles textinput, spinner, key
Config github.com/BurntSushi/toml ~/.synapse-admin/config.toml
HTTP stdlib net/http no extra dependency
Go version 1.22+

Config format (~/.synapse-admin/config.toml)

[server]
homeserver = "https://matrix.example.com"
access_token = "syt_admin_xxxxxx"

File is created with mode 0600. Never log the access token.

Message flow

User keyboard/mouse
       │
       ▼
RootModel.Update()          ← only place tea.Msg is processed
       │
       ├─ dialog open? → forward all keys to activeDialog.Update()
       │
       ├─ global keys (ctrl+p, ctrl+s, q) → handle here
       │
       ├─ root-level action messages → open confirm dialog / run API mutation
       │
       └─ else → activeView.Update(msg) returns tea.Cmd
                         │
                         ▼
               goroutine via tea.Cmd
               api.Client.*()  (non-blocking)
                         │
                         ▼
  view-loaded messages | ok/error messages | action/request messages
                         │
                         ▼
               RootModel.Update() picks up result, updates state

All API calls are wrapped in tea.Cmd background work. The UI never blocks.

Dialog system

dialog.Dialog interface:

type Dialog interface {
    Update(tea.Msg) (tea.Model, tea.Cmd)
    View() string
}

RootModel.activeDialog is nil when no dialog is open. When set, all keyboard events are forwarded to it. Dialogs communicate back via typed messages:

Message Sender Meaning
dialog.CancelMsg any dialog user pressed esc / cancelled
dialog.ConfigSavedMsg config dialog credentials saved, client should be rebuilt
dialog.ConfirmMsg{Confirmed: true, OnConfirm: cmd} confirm dialog user confirmed, run cmd
dialog.CommandPaletteMsg command palette user selected a scope or command

View interface

func (v *XxxView) SetSize(w, h int)
func (v *XxxView) SetClient(c *api.Client)
func (v *XxxView) Load() tea.Cmd
func (v *XxxView) Update(msg tea.Msg) tea.Cmd
func (v *XxxView) View(height int) string

Views own their pagination state (from, nextToken, prevBatch, local history).

Status / error display

Three channels, all on the bottom bar:

  • RootModel.errMsg — red, auto-clears after 4 s via ClearStatusMsg
  • RootModel.okMsg — green, same timer
  • RootModel.loading + spinner — shown while any async load is in flight

Never use panic or log.Fatal in UI code. Return an ErrorMsg instead.


Key bindings

Global

Key Action
ctrl+p open command palette
ctrl+s open config dialog directly
q / ctrl+c quit
esc close dialog / cancel filter

Navigation

Key Action
/ k move up
/ j move down
n / next page
p / previous page
enter confirm

Users view

Key Action
/ search users through the Synapse API
r planned: reset password
x deactivate + erase selected user
a reactivate selected user
n next page
p previous page from local history

Rooms view

Key Action
/ search rooms through search_term
m planned: members overlay
d delete room, retry with force purge on failure

Tokens view

Key Action
c planned: create token
d planned: delete token

All bindings are defined in internal/ui/model/keys.go. Add new bindings there first, then wire them in root.go or the relevant view.


Synapse Admin API coverage

Phase 1 (current)

Endpoint Method Implemented in
/_synapse/admin/v2/users GET api/users.go ListUsers
/_synapse/admin/v2/users/{userId} PUT api/users.go ReactivateUser
/_synapse/admin/v1/reset_password/{userId} POST api/users.go ResetPassword
/_synapse/admin/v1/deactivate/{userId} POST api/users.go DeactivateUser
/_synapse/admin/v1/rooms GET api/rooms.go ListRooms
/_synapse/admin/v1/rooms/{roomId}/members GET api/rooms.go GetRoomMembers
/_synapse/admin/v1/rooms/{roomId} DELETE api/rooms.go DeleteRoom
/_synapse/admin/v1/registration_tokens GET api/tokens.go ListTokens
/_synapse/admin/v1/registration_tokens/{token} DELETE api/tokens.go DeleteToken
/_synapse/admin/v1/registration_tokens/new POST api/tokens.go CreateToken

Phase 2 (planned)

  • GET/PUT /_synapse/admin/v2/users/{userId} — user detail & edit form
  • PUT /_synapse/admin/v2/users/{userId} — create user flow
  • GET /_synapse/admin/v1/server_version — server info screen
  • GET /_synapse/admin/v1/statistics/users/media — storage stats per user
  • Media quarantine / delete endpoints
  • Background task polling (GET /_synapse/admin/v1/background_updates/status)
  • Multi-profile support in config

Styles

All visual constants live in internal/ui/styles/styles.go. Use lipgloss.AdaptiveColor for every color so the app works in both light and dark terminals.

Color palette:

Name Light Dark Usage
ColorPrimary #5D5FEF #7B7DEF command palette, borders, selected items
ColorDanger #CC3333 #FF6B6B errors, deactivate, delete
ColorSuccess #2D8A4E #4ADE80 ok messages
ColorMuted #999999 #666666 secondary text, help bar
ColorBorder #CCCCCC #333333 panel borders, separators
ColorSelected #E8E8FF #2A2A5A highlighted row

Do not add inline lipgloss.NewStyle() calls in view or model files. Define the style in styles.go and import it.


Testing guidelines

  • Use testing + testify/require for assertions.
  • Enable t.Parallel() in every test.
  • Use t.TempDir() for any file I/O.
  • Mock the API by passing a *httptest.Server URL as homeserver.
  • Never make real network calls in tests.
  • Test file naming: foo_test.go in the same package as the file under test.
  • Golden files for rendered TUI output: testdata/*.golden, regenerate with -update flag.

Code style

  • Formatter: gofumpt (stricter than gofmt).
  • Import grouping: stdlib → external → internal (separated by blank lines).
  • Naming: PascalCase for exported, camelCase for unexported.
  • Comments: complete sentences, start with capital, end with period.
  • Error strings: lowercase, no trailing punctuation (fmt.Errorf("list users: %w", err)).
  • Commits: feat:, fix:, refactor:, chore:, docs: prefixes.

Work plan

Phase 1 — Feature parity with simple-synapse-admin HTML

Status: runnable TUI skeleton implemented.

Milestone 1.1 — Views

  • internal/ui/views/users.go — table rendering, up/down selection, next-page pagination plus local previous history, API-backed search
  • internal/ui/views/rooms.go — table rendering, prev/next pagination, search (/ opens inline filter, sent as search_term query param)
  • internal/ui/views/tokens.go — table rendering, no pagination

Milestone 1.2 — Dialogs

  • internal/ui/dialog/config_dialog.go — homeserver/access-token form
  • internal/ui/dialog/confirm_dialog.go — generic confirmation dialog
  • internal/ui/dialog/command_palette.goctrl+p scope/action palette
  • internal/ui/dialog/password_dialog.go — masked password input
  • internal/ui/dialog/members_dialog.go — read-only room members overlay

Milestone 1.3 — Wire actions

  • Users view: r key → open password dialog for selected user
  • Users view: x key → confirm dialog → api.DeactivateUser
  • Users view: a key → confirm dialog → api.ReactivateUser
  • Rooms view: m key → open members dialog for selected room
  • Rooms view: d key → confirm dialog → api.DeleteRoom, retry with force purge on failure
  • Tokens view: d key → confirm dialog → api.DeleteToken
  • Tokens view: c key → open create-token dialog

Milestone 1.4 — Command UX / Help

  • ? key toggles a full-screen help overlay
  • Bottom status bar shows relevant bindings for the current view
  • ctrl+p command palette is the primary way to switch scope

Milestone 1.5 — Polish & release

  • First-run UX: auto-open config dialog when no config exists
  • version injected at build time via -ldflags
  • README.md with install instructions, config, and keybindings
  • Makefile or Taskfile.yaml
  • GitHub Actions CI

Phase 2 — Extended admin capabilities

  • Multi-profile config: [[profiles]] array in TOML
  • User create form
  • User detail / edit view
  • Server info screen
  • Media management
  • Background task status bar
  • Bulk selection with space
  • Local action log
  • Export selected table to CSV

Common pitfalls

Pagination asymmetry. The v2 users API is forward-only via next_token. The app simulates previous navigation with a local history of visited pages.

Room deletion is async. The DELETE endpoint returns immediately; actual purge happens in the background. Treat the initial 200 response as accepted, not finished.

Deactivate ≠ delete. Synapse never removes the user row completely. Deactivation + erase removes personal data but the user record remains visible.

Activation may require a password. Reactivation is currently wired through PUT /_synapse/admin/v2/users/{userId} with deactivated=false. Some homeservers may require additional fields for local-password users.

Config file permissions. Always open with os.O_WRONLY|os.O_CREATE|os.O_TRUNC and mode 0600. Never write the access token to stdout or logs.