Zenbu

hg-web
Login

hg-web

A custom Mercurial forge and repository browser. The application keeps the networking and HTTP stack in C with Seobeo, while the hand-drawn web interface is implemented in React and TypeScript.

Wiki tree

hg-web
├── Runtime
│   ├── hg_web_server :6970
│   │   ├── React application and static assets
│   │   ├── repository browsing API
│   │   ├── commit graph API
│   │   └── Mercurial wire-protocol proxy
│   └── hg serve :4444
│       ├── repository files and graph data
│       └── Mercurial pull/push protocol
├── Backend
│   ├── main.c
│   └── ../seobeo
│       ├── s_network.c
│       ├── s_web.c
│       ├── s_http_client.c
│       └── s_ssl.c
├── Frontend
│   └── src
│       ├── main.tsx
│       ├── components
│       │   ├── app.tsx
│       │   ├── directory-browser.tsx
│       │   ├── graph.tsx
│       │   ├── header.tsx
│       │   ├── footer.tsx
│       │   ├── theme.tsx
│       │   └── repo-browser.tsx
│       ├── index.html
│       ├── index.css
│       ├── base.css
│       └── custom pencil, panda, and icon assets
├── Build
│   ├── BUILD
│   ├── ../gui_ze/gui_ze.bzl
│   ├── ../markdown_converter
│   └── ../third_party/highlight
└── Operations
    └── deploy.sh

repo-browser.tsx is an older standalone browser implementation. The active entry point is main.tsx, which renders app.tsx; app.tsx embeds directory-browser.tsx.

Runtime topology

Browser
  |
  | HTTPS
  v
nginx
  |
  | HTTP :6970
  v
hg_web_server (main.c + Seobeo)
  |
  | HTTP :4444
  v
hg serve (Zenbu Mercurial repository)

The C server owns the public routes. It serves the application shell and assets, translates browser API requests into hg serve requests, and streams Mercurial wire-protocol traffic without routing bundle data through the normal buffered HTTP response path.

Request flows

Application shell

GET /, /directories, or /graph
  -> GetReactHome
  -> hg-web/src/index.html
  -> /page.js
  -> src/main.tsx
  -> components/app.tsx

Repository browser

directory-browser.tsx
  -> GET /api/repo/list?path=...
  -> ApiListDirectory
  -> GET hg-serve/file/tip/<path>?style=json

file or README selection
  -> GET /api/repo/file?path=...
  -> ApiGetFile
  -> GET hg-serve/raw-file/tip/<path>
  -> highlight.js, markdown_converter WASM, or inline static preview

Images, SVG, video, audio, and PDF files open in the forge preview modal. Unknown binary files retain a download link. The raw file API supplies explicit MIME types, nosniff, inline disposition for supported previews, and sandboxed SVG responses.

Commit graph

graph.tsx
  -> GET /api/graph/<revision>?graphtop=...&style=json
  -> ApiGetGraph
  -> GET hg-serve/graph/<revision>?...
  -> React rows + custom pencil/panda canvas

graph row selection
  -> /changeset/<revision>
  -> GET /api/changeset/<revision>
  -> ApiGetChangeset
  -> GET hg-serve/json-rev/<revision>
  -> changeset metadata + rendered diff

Mercurial clone, pull, and push

Mercurial client
  -> GET or POST /repo
  -> StreamHgWireProtocol
  -> hg serve /?cmd=...
  -> binary-safe streamed response

Route map

Method Route Owner Purpose
GET / GetReactHome Application shell
GET /directories GetReactHome Legacy application-shell route
GET /directory GetReactHome Repository browser application route
GET /graph GetReactHome Commit graph application route
GET /changeset/:changeset_id GetReactHome Changeset application route
GET /api/repo/list ApiListDirectory Directory listing JSON
GET /api/repo/file ApiGetFile Raw tracked file
GET /api/repo/readme ApiGetReadme Directory README content
GET /api/graph/:graph_id ApiGetGraph Mercurial graph JSON
GET /api/changeset/:changeset_id ApiGetChangeset Changeset metadata and diff JSON
GET, POST /repo StreamHgWireProtocol Mercurial wire protocol

Frontend ownership

File Responsibility
src/components/app.tsx Client-side routes, landing page, tabs, and history
src/components/directory-browser.tsx Breadcrumbs, listings, prefetch, file modals, README rendering
src/components/graph.tsx Graph fetching, pagination, canvas edges, panda nodes
src/components/theme.tsx Stored light/dark preference and system-theme integration
src/components/header.tsx Forge identity and theme control
src/components/footer.tsx Shared footer
src/index.css, src/base.css Custom visual language and layout

Keep the custom assets and visual language in this package. New forge screens should reuse the existing CSS variables, typography, textures, and components instead of introducing a generic design system.

Build and local run

Build the server and deployable bundle from the repository root:

bazel build //hg-web:hg_web_server
bazel build //hg-web:hg_web_server_bundle
bazel test //hg-web:tests
bazel test //markdown_converter/tests:markdown_to_html_test
bazel test //seobeo/tests:all

//hg-web:tests creates a temporary two-commit Mercurial repository and runs the application in pinned Chromium through Playwright. It covers shell routes, files and README rendering, graph and changeset navigation, browser history, console and page errors, API validation, Mercurial wire protocol, Markdown script escaping, static assets, and backend outages. The test invokes the workspace Mercurial CLI with an isolated configuration and home directory.

Build and run the complete local stack with one Bazel command:

bazel run //hg-web:dev

This starts hg serve on 127.0.0.1:4444 and hg_web_server on port 6970. The hg serve process comes from //third_party/mercurial:runtime and points at the current workspace. Stopping the Bazel target stops both child processes.

The bundle contains hg_web_server, hg-web/run_hg_web, a pinned pure-Python Mercurial runtime, and hg-web/src/, including generated page.js, markdown WASM, highlight.js, styles, and image assets.

Deployment contract

The expected production layout is:

nginx
  -> hg_web_server.service
     -> /opt/hg_web_server/active/hg-web/run_hg_web
        -> bundled Mercurial 7.2.3 serves
           /opt/hg_web_server/repository on 127.0.0.1:4444
        -> hg_web_server serves the forge on 127.0.0.1:6970

deploy.sh follows the same new/active bundle swap as the personal site. It builds the optimized bundle and maintains a canonical repository outside the release directories. The first deploy creates it with a locking-aware local clone. Later deploys create a Mercurial bundle from the workspace and pull that bundle into the persistent repository, preserving any commits pushed directly to production. The script verifies the repository, installs the systemd unit, swaps the active directory, restarts the combined service, and checks http://127.0.0.1:6970/. A failed restart restores the previous active directory.

There is no separate production hg serve service and no dependency on the host's hg command. The pinned Mercurial wheel requires CPython 3.11. The systemd unit uses KillMode=control-group, so both the Mercurial and C child processes stop together.

On its first run, deploy also installs /etc/nginx/sites-available/hg-web when Nginx uses Debian-style sites-available/sites-enabled directories. Existing site files are left untouched.

NGINX_SERVER_NAME=zenbu.babocoder.com bazel run //hg-web:deploy

The checked-in template listens on origin HTTP port 80, which works when TLS is terminated upstream. For direct origin TLS, provide a complete certificate-aware template:

NGINX_SITE_TEMPLATE=/etc/nginx/templates/hg-web-tls.conf \
  bazel run //hg-web:deploy

Set MANAGE_NGINX=false to leave Nginx untouched. NGINX_SITE_NAME, NGINX_SITE_AVAILABLE, and NGINX_SITE_ENABLED can override the destination names and paths. DEPLOY_ROOT controls the validated deployment root; REPOSITORY_PATH can override the persistent canonical repository within it.

Pushes are disabled by default. HG_ALLOW_PUSH=true enables unauthenticated pushes in the internal hg serve; only use it when /repo is protected by Nginx authentication or another trusted access layer.

Publishing

Push committed Mercurial changes to the configured server:

hg push

Then update and deploy on the server:

ssh -t [email protected] \
  'cd ~/zenbu && hg update default && bazel run //hg-web:deploy'

The SSH key may prompt for its passphrase. //hg-web:deploy builds the release bundle through Bazel before running the atomic promotion, health check, and rollback workflow.

Forge capability tree

Zenbu Forge
├── Repository
│   ├── File and directory browsing            available
│   ├── Syntax highlighting                    available
│   ├── README rendering                       available
│   ├── Commit graph                           available
│   ├── Changeset detail and diff              available
│   ├── Branches, bookmarks, and tags          planned
│   ├── File history and blame                 planned
│   └── Search                                 planned
├── Collaboration
│   ├── Authentication and authorization       planned
│   ├── Changeset review                       planned
│   ├── Issues                                 planned
│   └── Releases and artifacts                 planned
└── Automation
    ├── Mercurial incoming/changegroup hook    planned
    ├── Durable SQLite job queue               planned
    ├── Isolated Bazel runner                  planned
    ├── Live logs and job status API           planned
    ├── Forge status and log screens           planned
    ├── Artifact retention                     planned
    └── Atomic deploy, health check, rollback  available

Automation data flow

The Actions-like subsystem should extend the existing server rather than replace it:

hg push
  -> Mercurial hook records repository + revision
  -> small enqueue command writes a SQLite job
  -> runner service claims one queued job
  -> isolated shared checkout updates to the exact revision
  -> Bazel build and test steps stream logs
  -> job result and artifacts are recorded
  -> hg-web status API exposes the result
  -> custom React screens render runs, steps, logs, and artifacts
  -> successful protected jobs may call atomic deployment

Start with one runner on the same host. Keep the hook fast, never execute build steps inside the Mercurial request, and make the queue durable before adding parallel or remote runners.

Safe extension order

  1. Add branches, bookmarks, tags, file history, blame, and search.
  2. Add the hook, SQLite queue, single runner, and retained logs.
  3. Add run/status/log pages using the existing custom UI.
  4. Add authentication and authorization before accepting public pushes or user-defined automation.

Invariants