Panel
The Fediversity Panel is the operator-facing web frontend for managing
deployments, written in Django and living under panel/. It renders the
operator configuration schema as a form, submits the resulting configuration to
the API
over HTTP, and subscribes the browser to deployment-state events via Centrifugo
over WebSocket.
Where it sits
The panel is a thin user-facing layer over the deployment machinery:
- The form it presents is generated from the operator configuration schema (the
tf-incus-hostssetup's configuration type), converted to a JSON schema bycore/lib/schema-for-source.nix. The form fields and the Nix options stay in sync by construction. - On submit, it forwards the configuration to the API, which queues and runs the deployment. The panel itself does not build or deploy anything; it is a client of the API.
- It enforces per-operator authorization on the API's Centrifugo pub/sub callbacks, so an operator only sees events for their own deployments.
Besides the whole-configuration form at deployments/<pk>/, the panel serves a
per-application one at deployments/<pk>/<application>/, rendering that
application's options alone. It edits a slice of the same deployment -- the
settings under the chosen deployment method -- and saves and deploys the whole
configuration, so it is a zoom rather than a second source of truth. A
deployment method has to be picked on the deployment page first, since that is
where an application's settings live. The narrowing is the API's
?application= schema view; the panel holds no list of applications of its own.
See panel/README.md.
The form is not Django-rendered
Django serves the page, but not the form on it. That is the forms-island
bundle, built by
panel/nix/package.nix
from the nix-module-form
npins pin, with
panel/forms-island-adapter/panel-adapter.ts
as the bundle entry -- so the library itself carries no panel knowledge and the
panel consumes only the two built static files.
The island mounts into #deployment-form-island in
deployment_form.html
and fetches the JSON schema and the rename table from the API in the browser.
Django supplies only the hidden id_deployment_value input the island writes
its value back into, and the Configuration-source textarea. The same island
backs the per-application form; only the schema it is handed is narrower.
Screenshots
The operator's path through the panel, at the five views the deploy family
serves, in the light and the dark theme. The screenshots package captures
them from the running UI and the publish lays them over these pages, so they
show the revision that built the site:
| Light | Dark |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
The whole-configuration form at deployments/<pk>/, scrolled past the header to
the island itself: the deployment-method picker and every application section
under it are the forms-island bundle, not Django. The panel chrome and the
island follow the same theme, which the chrome broadcasts to the island.
| Light | Dark |
|---|---|
![]() |
![]() |
The per-application form at deployments/<pk>/<application>/, the same island
handed a narrower schema:
| Light | Dark |
|---|---|
![]() |
![]() |
Deployment identifiers
The API addresses deployments by an opaque {username}/{pk} identifier. The
panel maps these to its Django models: username is the operator's
User.username and pk is the Deployment primary key. A deployment with pk
42 owned by alice is alice/42 in API URLs -- so submitting it is
POST /deployments/submit/alice/42 -- and maps to the Centrifugo channel
fediversity.deployments.alice/42.
The panel's three Centrifugo proxy views (CentrifugoConnect,
CentrifugoSubscribe and CentrifugoPublish) enforce ownership: a subscribe
request for fediversity.deployments.<user>/... is allowed only when <user>
matches the Django session user.
Running and developing the panel
The panel is a Django project meant to use by Nix: enter its dev shell with
nix-shell in panel/, then drive it with the manage wrapper (manage migrate, manage runserver, manage createsuperuser). Running it against a
live API and Centrifugo, and provisioning the deployer's incus credentials so
the "Deploy" button works end to end, are operational concerns maintained
alongside the code: see panel/README.md
for the current setup, dev-shell, and local spin-up instructions.
Run these from the panel/ devShell, where .# resolves to the panel flake;
from the repo root write them as ./panel#manage /
./panel#checks.x86_64-linux.panel-basic (the thin root entrypoint re-exports
only the deploy family). Without flakes, run any app via its matching dev shell
-- e.g. nix-shell -A devShells.x86_64-linux.manage --run 'manage migrate' default.nix is the non-flake equivalent of nix run .#manage -- migrate.
Panel tests run with nix build .#checks.x86_64-linux.panel-basic.
Options
The services.panel NixOS options below configure a panel deployment.
services.panel.enable
Whether to enable Service configuration for panel.
Type: boolean
Default:
false
Example:
true
Declared by:
services.panel.apiBaseUrl
Base URL at which api/'s HTTP surface is reachable, used from both ends.
The browser reads it from <body data-api-base-url> for the
deployment form’s schema and rename-table fetches and the
Deploy/Delete calls. api/ serves those routes and the panel does not,
so leaving this empty sends them same-origin to the panel, they 404,
and the form never mounts.
The panel process uses it for its own server-side calls: the
deployment-state poll behind the Deploy/Destroy spinners, the
config-diff baseline, and the deployment list proxy. Left empty those
fall back to http://127.0.0.1:8088, which resolves only while api/
is colocated.
Normally an absolute origin, since api/ lives on its own registrable domain for white-labelling. It is only ever used as a string prefix, never parsed as a URL, so a panel-proxied path prefix works too.
Type: string
Default:
""
Declared by:
services.panel.centrifugo.enable
Whether to run a Centrifugo pub/sub server colocated with the panel.
Type: boolean
Default:
false
Declared by:
services.panel.centrifugo.apiUrl
Base URL of Centrifugo’s HTTP API. Used by api/ to publish events.
Type: string
Default:
"http://\${cfg.centrifugo.host}:\${cfg.centrifugo.port}/api"
Declared by:
services.panel.centrifugo.host
Bind address for Centrifugo’s HTTP server.
Type: string
Default:
"127.0.0.1"
Declared by:
services.panel.centrifugo.port
Bind port for Centrifugo’s HTTP server.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
8090
Declared by:
services.panel.centrifugo.wsDomain
Public hostname browsers use to reach Centrifugo over WebSocket.
Defaults to an events. subdomain so the panel session cookie
(scoped to the parent domain) is sent on the WS handshake.
Type: string
Default:
"events.\${cfg.domain}"
Declared by:
services.panel.domain
Public domain the panel is served on. Used for the nginx virtual host,
the session-cookie domain, and the default ALLOWED_HOSTS.
Type: string
Declared by:
services.panel.extra-settings
Django configuration written in Python verbatim.
Contents will be appended to the definitions in settings.
Type: strings concatenated with “\n”
Default:
""
Declared by:
services.panel.host
Bind address for the panel’s ASGI (uvicorn) server.
Type: string
Default:
"127.0.0.1"
Declared by:
services.panel.oidc.enable
Whether to enable single sign-on (OpenID Connect) login for the panel UI.
Type: boolean
Default:
false
Example:
true
Declared by:
services.panel.oidc.clientID
Client ID the panel is registered under with the provider.
Type: string
Default:
"panel"
Declared by:
services.panel.oidc.clientSecretFile
Path to a file holding the client secret. Read by a privileged
oneshot (panel-oidc-secret.service) which stages a
panel-readable copy, so the file itself need not be readable by the
panel user and may appear, or change, only after boot: a path
unit re-stages it on every write and restarts the panel when the
copy changed. Empty (or a file that never appears) leaves SSO
unusable but still starts the panel on password login.
Type: string
Default:
""
Declared by:
services.panel.oidc.displayName
Name of the identity provider as shown on the login page’s “Log in with …” button.
Type: string
Default:
"single sign-on"
Declared by:
services.panel.oidc.issuer
OIDC issuer URL. The panel discovers every endpoint from
${issuer}/.well-known/openid-configuration, so nothing else about
the provider needs configuring.
Type: string
Default:
""
Declared by:
services.panel.oidc.scopes
OIDC scopes the panel requests.
Type: list of string
Default:
[
"openid"
"profile"
"email"
]
Declared by:
services.panel.port
Bind port for the panel’s ASGI (uvicorn) server.
Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)
Default:
8000
Declared by:
services.panel.production
Whether this is a production deployment. Enables ACME/TLS on the
panel’s nginx virtual hosts and serves Centrifugo over wss://.
Type: boolean
Default:
true
Declared by:
services.panel.restart
systemd restart behavior
Type: one of “no”, “on-success”, “on-failure”, “on-abnormal”, “on-abort”, “always”
Default:
"always"
Declared by:
services.panel.secrets
Secrets to stage for the panel service via systemd LoadCredential,
as a mapping from credential name to source file path. SECRET_KEY
is populated by default from the generated panel secret.
Type: attribute set of absolute path
Default:
{ }
Declared by:
services.panel.settings
Django configuration as an attribute set. Name-value pairs will be converted to Python variable assignments.
Type: attribute set of anything
Default:
{
STATIC_ROOT = "/var/lib/panel/static";
DEBUG = false;
ALLOWED_HOSTS = [ cfg.domain cfg.host "localhost" "[::1]" ]
++ lib.optional cfg.centrifugo.enable cfg.centrifugo.wsDomain;
CSRF_TRUSTED_ORIGINS = [ "https://${cfg.domain}" ]
++ lib.optional cfg.centrifugo.enable "https://${cfg.centrifugo.wsDomain}";
COMPRESS_OFFLINE = true;
LIBSASS_OUTPUT_STYLE = "compressed";
}
Declared by:
services.panel.shellEnv
The environment from which to execute the manage script.
Type: attribute set
Default:
{ }
Declared by:
services.panel.windmill.baseUrl
Base HTTP URL of the Windmill instance (e.g. http://windmill:8000).
Required for deployments; leave empty to disable job submission.
Type: string
Default:
""
Declared by:
services.panel.windmill.tokenFile
Path to a file containing the Windmill API token. The file must be readable by the panel service user.
Type: string
Default:
""
Declared by:
services.panel.windmill.workspace
Windmill workspace identifier.
Type: string
Default:
"fediversity"
Declared by:









