-
Notifications
You must be signed in to change notification settings - Fork 3
web ui
Pre-Alpha. This page describes behavior that may change.
The web UI is the same engine as the CLI with a different surface. It reads and writes the same config tree, runs the same YANG validators, and uses the same user database. This is the long version of the Web UI tour.

The web server is part of the Ze daemon. You enable it from the command line or in the config.
ze start --web 3443environment {
web {
enabled true;
server main {
ip 0.0.0.0;
port 3443;
}
}
}
When no certificate is configured, Ze generates an ECDSA P-256 self-signed certificate on first start, with SANs for localhost, 127.0.0.1, ::1, and the listen address. Put a real certificate in front of the port before you expose it.
The --insecure-web flag turns off authentication and forces the listener back to 127.0.0.1. It exists for development. Do not use it on a public interface.
The web server uses the same user database as the SSH CLI. You can log in two ways.
Browser sessions go through /login. On success, Ze sets a ze-session cookie that is Secure, HttpOnly, and SameSite=Strict. Each user has at most one active session: logging in from a second browser invalidates the first.
API clients use HTTP Basic. Send Accept: application/json (or append ?format=json to the URL) and Ze authenticates the request without creating a session.
Both login paths run through the same AAA chain as the SSH CLI, so RADIUS and TACACS+ administrators can sign in to the web UI. Local users remain the fallback, used when the remote servers are unreachable or do not recognise the account.
curl -k -u admin:password https://localhost:3443/show/bgp/?format=jsonEvery response carries the security headers you would expect: HSTS (max-age=63072000; includeSubDomains), Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self', X-Frame-Options: DENY, and X-Content-Type-Options: nosniff. An authenticated response adds Cache-Control: no-store and X-Ze-Version.
script-src 'self' is load-bearing, not decoration. It refuses an inline script and it refuses Function(), so no page carries an inline event handler and no htmx attribute uses a bracketed trigger filter. Tests refuse both in any template source, after a filter that the CSP silently dropped turned an Enter-only trigger into a bare keyup and sent one POST per keystroke.
A session lasts 24 hours, and ends earlier if the configuration stops declaring the user. Remove a user and reload, and their open browser tab is refused on its next request, with no daemon restart. A user authenticated by RADIUS or TACACS+ keeps their session: the local user list did not grant it and does not end it.
A session's authorization profile decides what it can reach. A read-only session (a profile that denies the edit section) is blocked from every configuration-editing surface: the config edit form and the /admin/ console return 403 Forbidden, and the edit controls are hidden from the navigation so they are never offered in the first place.
The same authorizer enforces both layers. The route gate that returns the 403 and the handler that would apply a mutation consult one authorizer, so page visibility and mutation enforcement never diverge. Read paths are unaffected: a read-only user still browses the config tree, runs show and monitor, and downloads the running config.
Deployments with a single admin and no authorization assignments are unaffected: with nothing to enforce, the gate allows edits.
The URL pattern is verb-first and three-tier.
| Tier | Pattern | Method | Description |
|---|---|---|---|
| View | /show/<yang-path> |
GET | Read-only config tree view. |
| View | /monitor/<yang-path> |
GET | Same as /show with auto-polling. |
| Config | /config/edit/<path> |
GET | Editable config tree. |
| Config | /config/set/<path> |
POST | Set a leaf value. |
| Config | /config/add/<path> |
POST | Create a list entry. |
| Config | /config/rename/<path> |
POST | Rename a keyed list entry. |
| Config | /config/delete/<path> |
POST | Delete the node named by the leaf form field: a leaf, a leaf-list member, a container, a whole list, or one list entry. |
| Config | /config/add-form/ |
GET | Add new entry overlay form. |
| Config | /config/changes |
GET | Commit bar HTML with pending change count. |
| Config | /config/commit/ |
GET/POST | Diff and commit. |
| Config | /config/discard/ |
POST | Discard pending changes. |
| Config | /config/compare/ |
GET | Compare pending against committed. |
| Config | /config/diff |
GET | Diff modal with content. |
| Config | /config/diff-close |
GET | Closed diff modal (empty state). |
| Config | /config/download |
GET | Download the committed config as a text attachment. |
| Config | /config/upload |
POST | Replace the whole config from an uploaded file. |
| Fragment | /fragment/detail |
GET | HTMX fragment for detail panel. |
| SSE | /events |
GET | SSE endpoint for real-time config change notifications. |
| Admin | /admin/<yang-path> |
GET/POST | Administrative commands. |
| Auth | /login |
POST | Login (no auth). |
| Static | /assets/ |
GET | CSS, JS, images (no auth). |
/ redirects to /show/.
The left panel is a Finder-style column browser, the kind macOS users will recognise. You click into a container, a new column slides in to the right, and you keep going until you hit a leaf or a list. Three columns are visible at a time, scrolling horizontally as you go deeper.
Named containers (lists with YANG keys, like peer and group) appear above unnamed ones (local, timer), separated by a horizontal rule. Lists with unique constraints render as a table in the detail panel, with the key and unique fields as columns. You rename, edit, and delete entries inline. The + new button under every list opens a server-rendered form, with YANG types validated before the entry is created.
When you are inside a list entry, the detail panel shows a context heading at the top (PEER upstream) so you do not need the breadcrumb to know where you are.
Each authenticated user gets an independent draft session. Your edits are tracked per user and never affect other users until you commit.
Text and number fields auto-save one second after you stop typing, in addition to saving on blur and Enter. Enter commits inside the debounce, so three keystrokes and an Enter send one POST, not four. The commit bar at the bottom of the page tracks the count of pending changes. Click "Review and Commit" to see a diff and either commit or discard.
Deleting works on whatever the delete control names, not on leaves alone. The delete button on a list row removes that entry, and the same endpoint takes a leaf, a leaf-list member, a container or a whole list. It dispatches by schema, which is what the list-row button needed: list entries do not live where a leaf lives, so the old leaf-scoped path answered "path not found" and the browser threw the answer away.
Conflicts with another user are detected at commit time. The conflict report names the paths and shows both your value and the other user's value. You resolve them and try again.
If a colleague commits while you have the page open, a Server-Sent Events stream pushes a notification banner with their username and a Refresh button. The SSE broker supports up to 100 concurrent clients.
A refused action raises a toast carrying the status and the message the daemon wrote, and the same message lands in the field or panel the action came from. A request that gets no answer at all raises a different toast: the daemon is unreachable, and nothing was changed. The two cases are told apart on the response, because htmx 4 requests with fetch and merges both into one error event.
The editor manager allows up to 50 concurrent draft sessions. Idle sessions (no activity for one hour) are evicted when capacity is reached.
You can move the whole configuration through the browser. GET /config/download streams the committed config as a ze.conf text attachment. Any authenticated session may download it, and the download is audit-logged.
POST /config/upload replaces the entire configuration from an uploaded file. The upload is validated exactly like a commit: an invalid config is rejected with the validation error (HTTP 400) and nothing is applied. A valid config is applied, the reload fires, and the action is audit-logged. Upload is an edit action, so read-only sessions are refused. The body is capped to reject oversized uploads.
No render path prints a value the schema marks ze:sensitive or ze:bcrypt. One predicate answers "does this leaf hold a secret", and both the display mask and the write guard read it, so the two halves cannot drift apart.
| Surface | What you see |
|---|---|
| The config tree, the diff, the compare view, the download | A secret that holds a value reads as a placeholder. An unset secret stays empty, so the field still reads as unconfigured. |
| The commit diff | A rotated secret is named as a changed path. Neither the old value nor the new one is printed. |
| The CLI bar and the terminal |
show masks the same leaves. show needs no config authorization, so before this any authenticated session could read them. |
| Commit, load, upload | A tree carrying the placeholder in a secret leaf is refused, rather than silently resolved. Restore the real value from the edit-authorized raw download, or set it through plaintext-<name> or ze passwd. |
That last row is the reason the guard reads the same predicate as the mask: a guard narrower than the mask beside it turns the mask into a way to destroy what it protects. A dump-then-upload round trip used to overwrite the stored config with the placeholder.
Every page has a CLI bar at the bottom that takes the same grammar as the SSH shell. The bar carries the current URL path as context, so a set or delete runs against whatever you are looking at. Tab completion is context-aware: at /show/bgp/peer/upstream/, typing set and Tab offers the children of that peer entry, not root-level items.
| Command | Effect |
|---|---|
edit <path> |
Navigate to a config path. |
set <leaf> <value> |
Set a value at the current context. |
delete <leaf> |
Delete a value at the current context. |
show [path] |
Display config text at the current or given path. |
top / up
|
Navigate to root or up one level. |
commit / discard
|
Apply or abandon pending changes. |
who |
List active web editing sessions. |
help |
List available commands. |
There is also a terminal mode for when you want a scrollback shell in the browser.
The /admin/ tree exposes administrative RPCs through the same finder-style column navigation as the config tree. GET renders the navigation and command forms; POST executes the command. The tree is derived from the ze-bgp-api YANG RPCs.
| Group | Subcommands |
|---|---|
peer |
list, show, summary, capabilities, statistics, add, remove, teardown, clear-soft, flush
|
route |
update, borr, eorr, raw
|
cache |
list, retain, release, expire, forward
|
system |
commit, subscribe, unsubscribe, events
|
Leaf commands (those with no sub-commands) render a parameter form in the detail panel. POST to /admin/<group>/<command> executes the command and returns the result.
The Workbench is now the default web UI. It provides:
- Two-level navigation: top bar for domains (Dashboard, BGP, Config, System, Specs), sidebar for sub-pages.
- Dashboard: hostname, uptime, version, memory usage at a glance. Live BGP summary data and SSE log stream.
- Domain pages: per-peer detail with capabilities, statistics, flush, and teardown controls. Configured interfaces with canonical kernel types.
- Integrated CLI page: full terminal-mode CLI in the browser.
- Structured log viewer: parses and displays JSON log output with filtering.
- Theme: unified CSS custom properties, switchable at runtime.
The previous Finder-style UI is still accessible by setting ZE_WEB_UI=finder. The --web-only flag on ze cli launches the daemon with only the web UI (no interactive CLI session).
Every page, panel, fragment and out-of-band swap is written in a .templ source and compiled to Go by templ. No Go file in the web package builds markup: the exemption table is empty, and a test holds it at zero. That is the point of the port. A renamed view-model field is now a compile error, where the old engine rendered a blank panel and answered 200.
htmx 4.0.0-beta6 is embedded. The same library serves the Looking Glass and the chaos dashboard, which also load hx-sse.min.js, htmx 4's SSE extension. The web UI does not: its live-update stream is a plain EventSource that listens for a named config-change event, so it needs no htmx extension.
Each page loads only the assets its component graph reaches, and the sets are generated rather than written down. Of the six pages in the web package, four load no htmx at all: the two L2TP views, the sign-in page, and the snapshot page.
Contributors editing a template run make generate, which regenerates every *_templ.go from its source and syncs the vendored browser assets. It needs no network and nothing on PATH, because the generator runs from vendor/. Run make generate rather than a bare templ generate: the walk root is written into the generated Go, so a bare run rewrites every file and reds the check.
| Target | What it does |
|---|---|
make generate |
Regenerate *_templ.go, then sync third_party/web/ to every consumer. |
make ze-templ-output-check |
Refuse a *_templ.go its source no longer produces. Writes nothing, deletes nothing. |
make ze-templ-orphan-check |
Report a *_templ.go whose source is gone. |
make ze-vendor-web-check |
Refuse a vendored asset copy that differs from its source. |
make ze-htmx-upgrade-check |
Run htmx's own upgrade scanner and refuse an unexplained issue. |
The UI ships an English base catalog and a French locale. The locale is chosen from the browser's Accept-Language header, and any key missing from a non-English catalog falls back to English, so a partial translation never renders a blank label.
The stylesheet carries a small-screen layout down to 390px (iPhone-class portrait), with a horizontal-scroll guard so the page body never scrolls sideways. Wide content scrolls inside its own container instead.
Some leaves are annotated at render time with a friendlier form beside the raw value:
| Value | Annotation |
|---|---|
| An IP address | Its reverse-DNS (PTR) hostname. |
A standard community in ASN:value form |
Its well-known RFC name (for example 65535:65281 shows as no-export). |
Decoration is best-effort: a failed DNS lookup, or a community with no well-known name, simply shows the raw value with no annotation. These join the existing ASN-name decorator, which annotates an AS number with its organization name.
The Looking Glass route-list API accepts limit and offset query parameters to page through large tables (for example ?limit=50&offset=100). Without them a request returns the full list unchanged. limit=0 means unlimited, an offset past the end returns an empty list, and a non-numeric or negative value is rejected with HTTP 400.
- Web UI tour for the five-minute version.
- CLI for the same operations through SSH.
- Configuration overview for the model both surfaces share.
Adapted from main/docs/features/web-interface.md and main/docs/guide/web-interface.md.
Unreviewed draft. This wiki was authored in bulk and has not been reviewed. File corrections on the issue tracker.
- Overview
- YANG Model
- Editor Workflow
- Archive and Rollback
- System
- Interfaces
- VRRP
- BFD
- FIB
- OSPF
- IS-IS
- MPLS / LDP / RSVP-TE
- RSVP-TE
- SRv6
- Static Routes
- Policy Routing
- Firewall
- Traffic Control
- Class of Service
- L2TP/PPP
- PPPoE
- VPP Data Plane
- RPKI
- IPsec VPN
- TACACS+ AAA
- RADIUS AAA
- AS112 DNS
- DNS
- Authorization
- Fleet
- BGP
- Starting and Stopping
- Show Commands
- Monitoring
- Flow Export
- DDoS Mitigation
- Anomaly Detection
- Health Checks
- Audit Trail
- Production Diagnostics
- Logging
- Operational Reports
- Healthcheck
- Self-Update
- Zero-Touch Provisioning
- MRT Analysis
- Upgrade and Restart
- Storage
- Policy
- Core
- Resilience
- Validation
- Capabilities
- Address Families
- Protocol
- Subsystems
- Infrastructure
- Route Server at an IXP
- Transit Edge with RPKI
- Public Looking Glass
- ExaBGP Migration Walkthrough
- FlowSpec Injection
- Chaos-Tested Peering
- AS Path Topology