Anso e380ee7b16 test(pilot): in-process integration test and Playwright E2E (#983)
* test(pilot): in-process integration test for the tunnel handshake

Layered unit tests cover the protocol decoder, the bridge cap, the
manager, and the DB-layer enrollment lifecycle. None exercise the
glue: the WebSocket dispatch order in upgradeHandler.ts, the actual
hello / enroll_ack round-trip, the long-lived token swap. A
regression that moves /api/pilot/tunnel below the auth gate, breaks
the hello / enroll_ack ordering, or changes the token shape would not
be caught today.

Spin up a real http.Server with attachUpgrade wired and a real
ws.WebSocket client. Three tests:

  - Enroll-ack happy path: connect with an enrollment token, receive
    hello + ctrl enroll_ack, assert the manager has registered the
    tunnel.
  - Replay rejection on the wire: connect, complete enroll-ack, close,
    reconnect with the same enrollment token, assert HTTP 401 from the
    upgrade handshake.
  - Long-lived token reconnect: capture the enroll_ack token, close,
    reconnect with the long-lived pilot_tunnel JWT, assert hello but
    no enroll_ack and the tunnel is registered.

The test does not mock the agent side beyond the framing protocol.
The goal is to confirm the wires connect end-to-end, not to drive a
full request round-trip (which would require a synthetic agent bridge
and adds little marginal coverage over the existing bridge unit tests).

This file also establishes the in-process WS-pair test pattern for
any future WebSocket integration work; no such pattern existed in the
suite before.

* test(e2e): cover operator-side pilot-agent enrollment in Playwright

Backend integration is covered by pilot-tunnel-integration.test.ts
and the vitest enrollment suites. The browser-only surfaces (mode
selector default, enrollment dialog, docker run code block,
regenerate affordance on an existing pilot-mode node) had no
automated coverage; a typo in the mode label, a styling regression
that hid the docker-run code, or a backend response shape change
that broke the rendered command would all ship unchecked today.

Two specs reusing the existing loginAs helper and the same
Settings -> Nodes navigation pattern from nodes.spec.ts:

  - Create flow: open Add Node, switch type to Remote, confirm pilot
    mode is the default (api_url field stays absent), submit, assert
    the enrollment dialog renders a docker run command containing
    SENCHO_MODE=pilot, SENCHO_PRIMARY_URL=, and a JWT-shaped
    SENCHO_ENROLL_TOKEN=. Cleanup deletes the row.
  - Regenerate flow: create a pilot node, capture the first token,
    open the row's edit dialog, click Regenerate enrollment token,
    assert the second token differs from the first. Cleanup deletes
    the row.

Out of scope for this E2E: simulating an agent connecting to flip the
row to Online. The integration test covers the wire side; this spec
keeps focus on the operator-visible UI.

Each test name-suffixes with Date.now() so re-runs do not collide on
the UNIQUE name constraint and so leftover rows from a partial run
get distinct names instead of stacking on the same row.

* fix(pilot): address PR B code-review findings

Three code-review findings:

  - High: NodeManager row action buttons were icon-only with no
    accessible name. Added aria-label="Edit node",
    aria-label="Delete node", aria-label="Test connection" so the
    Playwright E2E can target them by role/name (currently the
    accessible name is rendered into a Radix tooltip portal that
    Playwright cannot resolve as the button's name).
  - High: Replaced UI-driven cleanup in the E2E with API-based
    deletion via page.request, run in both beforeEach (sweep
    leftovers) and afterEach (sweep this test's row even on failure).
    Targets every node whose name starts with the test prefix so a
    crashed previous run cannot affect the next.
  - Medium: Replaced two fixed 50ms sleeps in the integration test
    with vi.waitFor polling on hasActiveTunnel(nodeId) === false,
    eliminating the race between the WS close hop chain and the next
    test step on slow CI runners.

Plus two cleanup items:

  - The integration test's afterAll now removes 'tunnel-up' /
    'tunnel-down' listeners from the manager singleton so this
    file's runs do not leak listeners into other test files in the
    same Vitest worker.
  - Tightened the WS error swallowing in the rejection-path test:
    only swallow the expected "Unexpected server response" error
    shape; surface anything else (ECONNREFUSED etc.) instead of
    silently masking it.

No em dashes added (Directive 18 verified clean).
2026-05-07 23:51:52 -04:00
2026-05-07 23:44:13 -04:00
2026-05-07 23:44:13 -04:00
2026-03-25 00:40:06 -04:00

Sencho

Self-hosted Docker Compose management for one machine or a fleet.

Docs · Website · Discussions · Sponsor

Latest release Docker Pulls CI License Discussions


Sencho dashboard

What Sencho is

Sencho is for homelab operators, small DevOps teams, and platform engineers who run services on Docker Compose, want a graphical interface without giving up file-on-disk workflows, and need to manage more than one machine without SSH gymnastics or a VPN.

It runs as a single container on your hardware and gives you a UI for the work you currently do over SSH on compose stacks: deploying, editing files, watching logs, restarting containers, browsing volumes, and recovering from failures. Your compose files stay on the host filesystem and remain the source of truth.

A Sencho instance is autonomous. To manage another machine, you install a second Sencho on it and connect them with a long-lived API token; the primary dashboard then acts as a transparent HTTPS proxy across your fleet. There is no SSH and no exposed Docker socket. For nodes behind NAT or strict firewalls, the Pilot Agent establishes a single outbound WebSocket tunnel to the primary, so the remote host opens no inbound port at all.

Most capabilities are free in the Community tier. A few advanced automation and fleet-control features ship in paid tiers; pricing lives at sencho.io/pricing.


Capabilities

Stacks

  • Full Compose lifecycle: create, deploy, restart, stop, pull
  • Monaco editor with diff preview before save and one-click rollback
  • Git-sourced stacks pulled and synced from any repository
  • File explorer for compose, env, and supporting files
  • Stack labels for grouping and bulk operations
  • App Store with LinuxServer.io templates

Observability

  • Aggregated log search and stream across every container in the fleet
  • Live container stats, health checks, and image-update notifications
  • Threshold alerts for CPU, memory, and network
  • Read-only audit log of every action
  • Network topology view of containers, networks, and nodes

Fleet

  • Multi-node management via authenticated HTTP and WebSocket proxy
  • Fleet view with grid and topology layouts
  • Fleet snapshots of compose and env across the fleet
  • Pilot Agent for nodes behind NAT or strict firewalls
  • Node compatibility checks before deploying

Automation

Security

Operations


Quick start

Sencho runs in a single container.

services:
  sencho:
    image: saelix/sencho:latest
    container_name: sencho
    restart: unless-stopped
    ports:
      - "1852:1852"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./data:/app/data
      # 1:1 Compose Path Rule: the host path MUST match the container path
      - /opt/docker:/opt/docker
    environment:
      - COMPOSE_DIR=/opt/docker
      - DATA_DIR=/app/data
docker compose up -d

Open http://your-server:1852 and create your admin account.

Always front Sencho with a TLS-terminating reverse proxy in production. See the self-hosting guide for hardening, environment variables, and reverse-proxy examples.

Run with docker run instead
docker run -d --name sencho \
  -p 1852:1852 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v sencho_data:/app/data \
  -e COMPOSE_DIR=/opt/docker \
  saelix/sencho:latest

For the full walkthrough, see the quickstart guide.


Adding remote nodes

To manage a second machine, install Sencho on it the same way, then add it from the primary dashboard with its URL and a long-lived API token. The primary proxies authenticated HTTPS and WebSocket requests to the remote instance. No SSH, no exposed Docker socket, no agent process on the remote. Nodes behind NAT or strict firewalls can opt into the Pilot Agent for outbound-only connectivity.

See the multi-node guide for the full token-bearer flow.


Screenshots

Stacks Editor
Fleet Logs

Documentation, community, and license


Contributors

S
Description
Self-hosted Docker Compose management platform. Great for homelabs, small DevOps teams, and platform engineers.
Readme AGPL-3.0 177 MiB
Languages
TypeScript 99.2%
JavaScript 0.4%
CSS 0.3%
Dockerfile 0.1%