diff --git a/README.md b/README.md
index bc50138..c9a950c 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,109 @@
+
+
# Runix
-Runix is an infrastructure management platform: one control plane manages a
-fleet of hosts through lightweight agents that dial out over WebSocket
-(NAT-friendly — nothing connects *in* to your servers). Manage Docker,
-Compose, systemd units and native daemons; browse and edit files; open
-terminals and consoles; schedule tasks; and update the whole fleet from the
-UI.
+**One console for your whole fleet.**
+Containers, system services and custom daemons — managed the same way, on every host.
-This repository holds the **release binaries and the installer**. The source
-lives in a separate repository.
+

+

+

+

+
+
+
+

+
+
+
+---
+
+This repository publishes the **release binaries and the installer**. The source
+lives in a separate, private repository.
+
+## What it is
+
+Runix manages servers the way you'd want to: you say *restart that thing*, and
+it doesn't matter whether "that thing" is a Docker container, a Compose
+project, a systemd unit or a plain process you wrote yourself.
+
+Everything is a **Runtime** with the same lifecycle — start, stop, restart,
+logs, metrics, a shell — so the interface stays the same as your stack changes.
+
+It ships as **two static binaries with no runtime dependencies**. The control
+plane serves its own web UI from inside the executable; agents dial *out* to it
+over WebSocket, so managed hosts need no inbound ports, no VPN and no public IP.
## Install
-One command sets up a host. Run it as root; it asks what the host should be
-(control plane, agent, or both), checks prerequisites, installs under
-`/opt/runix`, and wires up systemd.
+One command. It asks what the host should be, then does the rest:
```sh
curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh
```
-Binaries are served from object storage, so this needs no account and no
-token. To pin a version, add `--version v0.5.0`.
+```
+What should this host run?
+ 1) Control plane + agent (single-host install)
+ 2) Control plane only
+ 3) Agent only — join a control plane running elsewhere
+```
+
+Pick **1** and you get a working system from an empty machine: it provisions
+PostgreSQL in Docker, starts the control plane, registers the host with it, and
+installs the agent with a token it mints itself. Then open the URL it prints
+and log in.
+
+Downloads come from object storage and need no account, no token and no login.
+Every binary is checked against the release's `SHA256SUMS` before it is
+installed — a mismatch aborts.
### Roles
| Command | What it installs |
|---|---|
-| `sudo sh install.sh --role all-in-one` | Control plane **and** an agent on this host (single-host setup) |
-| `sudo sh install.sh --role server` | Control plane only |
-| `sudo sh install.sh --role agent --url https://runix.example.com --token ` | An agent that joins an existing control plane |
+| `install.sh --role all-in-one` | Control plane **and** an agent on this host |
+| `install.sh --role server` | Control plane only |
+| `install.sh --role agent --url https://runix.example.com --token ` | An agent joining an existing control plane |
-The all-in-one and server roles can provision PostgreSQL in Docker for you
-(`--db docker`) or use one you already run (`--dsn postgres://…`). Add `-y`
-for an unattended install that takes sensible defaults. `sh install.sh
---help` lists every flag.
+
+Unattended installs and options
+
+
+
+Every question is also a flag, so the same script works from Ansible or CI:
+
+```sh
+# Single-host install, no questions asked
+curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh -s -- \
+ --role all-in-one --yes
+
+# Agent joining an existing control plane
+curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh -s -- \
+ --role agent --url https://runix.example.com --token rnx_agt_...
+
+# Behind a reverse proxy: listen on loopback only
+curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh -s -- \
+ --role server --bind 127.0.0.1 --public-url https://runix.example.com -y
+```
+
+`-y` takes the recommended default for anything you didn't pass. Values with no
+safe default — the role, an agent's URL and token — fail loudly instead of
+being guessed.
+
+| Flag | Purpose |
+|---|---|
+| `--db docker` / `--dsn` | Provision PostgreSQL in Docker, or use one you already run |
+| `--bind`, `--port` | The IP and port the control plane listens on |
+| `--public-url` | Where browsers reach the UI (sets the CORS allow-list) |
+| `--version` | Install a specific release instead of the latest |
+| `--s3`, `--gitea` | Where to download from |
+| `--prefix` | Install root (default `/opt/runix`) |
+| `--no-start` | Configure everything, start nothing |
+
+`install.sh --help` lists them all.
+
+
Everything lives under one directory:
@@ -45,41 +114,218 @@ Everything lives under one directory:
/opt/runix/agent/ supervised daemon state (agent)
```
-## Update
+## Updating
-Re-running the installer upgrades in place — binaries are replaced and
-services restarted, while your configuration (above all the JWT and
-encryption secrets) is preserved:
+Re-running the installer upgrades in place. Binaries are replaced and services
+restarted, while your configuration — above all the JWT and encryption secrets
+— is preserved:
```sh
curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh -s -- -y
```
-Or update from the UI: an administrator sees an **Updates** page that checks
-object storage for the latest release and updates the control plane and each
-agent with one click. Point a control plane at this repository with:
+Configuration files are edited a key at a time rather than rewritten, so
+anything you added by hand survives, and an upgrade with no flags leaves them
+byte for byte identical.
+
+Or update from the UI: an administrator gets an **Updates** page that checks
+for the latest release and moves the control plane and each agent to it with
+one click. Point a control plane at this release channel with:
```
RUNIX_UPDATE_MANIFEST=https://runix-releases.s3-alpha-web.astra-dev.online/latest.json
```
+## What you get
+
+### Every workload, one interface
+
+Native daemons, Docker containers, Compose projects and systemd units, each in
+its own tab, all with the same actions. Runix supervises native daemons itself
+— restart policies, backoff, log capture — so a plain binary gets the same
+treatment as a container.
+
+
+
+### A console that actually takes input
+
+Not a log tail, and not a shell *next to* your process — the process's own
+stdin. Type `stop` into a game server and the game server sees it. Copy and
+paste work like they do in a terminal.
+
+
+
+### Per-host metrics and inventory
+
+CPU, memory, disk and load, live and historical, plus the hardware and OS
+details the agent reports on every heartbeat.
+
+
+
+### A real file manager
+
+Browse, edit with syntax highlighting, upload by drag-and-drop, download whole
+directories as `tar.gz`, create archives, change permissions — with tabs, a
+right-click context menu, sorting, search and multi-select. Transfers stream
+frame by frame, so a large file is never buffered in the control plane's
+memory.
+
+
+
+### Terminals, into the host or into a container
+
+A real PTY on the host, or a shell inside a running container, over the same
+outbound connection the agent already holds open. Sessions survive a page
+reload.
+
+
+
+### Docker, beyond containers
+
+Images, volumes and networks with disk usage and pruning — because they aren't
+runtimes and pretending otherwise would have bent the model out of shape.
+
+
+
+### Scheduling, with a cron engine of its own
+
+Restart something nightly, run a command weekly. Multiple control-plane
+instances can share a database without ever double-running a task.
+
+
+
+### Access control down to a single container
+
+Grant a user, a role or a group access to the whole fleet, one server, or one
+specific runtime on one specific host — including file access scoped to just
+that runtime's directory. Everything is a dropdown; nothing asks you to paste
+a UUID.
+
+
+
+### Audit log
+
+Who did what, to which resource, from which IP — recorded for every mutating
+action.
+
+
+
+### And the rest
+
+- **Auth** — argon2id, JWT with rotating refresh tokens and reuse detection,
+ TOTP MFA with recovery codes, personal access tokens
+- **Backup/restore** of configuration, deliberately without secrets
+- **Self-update** for the control plane and agents, refusing any download
+ without a matching checksum
+- **Plugins** — external processes speaking line-delimited JSON on stdio can
+ register entirely new runtime types
+- **English and Russian** UI
+
+## How it works
+
+```
+ Browser ──────► runix-server ──────► PostgreSQL
+ (control plane) Redis (optional, multi-instance)
+ ▲
+ │ agents dial OUT over WebSocket
+ │ (one connection: RPC + byte streams)
+ ┌─────────────┼─────────────┐
+ │ │ │
+ runix-agent runix-agent runix-agent
+ docker systemd daemons
+```
+
+- **Agents connect outbound.** Managed hosts need no open ports. One connection
+ multiplexes correlated RPC calls and byte streams (logs, terminals, file
+ transfers) at once.
+- **The runtime abstraction is small on purpose.** A core interface every
+ provider implements, plus optional capability interfaces it opts into. The
+ capability set travels to the UI, which renders only the actions that exist —
+ so no button is ever a "not supported" error waiting to happen.
+
+## Requirements
+
+| | |
+|---|---|
+| **OS** | Linux, `amd64` or `arm64` |
+| **Install** | root (the installer writes to `/opt` and `/etc/systemd/system`) |
+| **Service manager** | systemd (optional — without it the installer prints how to run the binaries) |
+| **Database** | PostgreSQL 17, provisioned in Docker for you or one you already run (control plane only) |
+| **Docker** | Optional. Needed to provision PostgreSQL and to manage Docker runtimes |
+
+Agents have no dependencies at all: one static binary.
+
+## Configuration
+
+Both binaries are configured by environment, read from `/opt/runix/etc/*.env`.
+
+
+runix-server — the essentials
+
+
+
+| Variable | Default | Description |
+|---|---|---|
+| `RUNIX_HTTP_ADDR` | `:8080` | Listen address; `127.0.0.1:8080` to sit behind a proxy |
+| `RUNIX_DATABASE_DSN` | — | PostgreSQL connection string (required) |
+| `RUNIX_JWT_SECRET` | — | Signing key for sessions (required; min 32 chars) |
+| `RUNIX_ENCRYPTION_KEY` | — | Encrypts stored secrets such as TOTP (required) |
+| `RUNIX_CORS_ORIGINS` | — | Browser origins allowed for API + WebSocket |
+| `RUNIX_UPDATE_MANIFEST` | this release channel | Where the Updates page looks for new versions |
+| `RUNIX_REDIS_ADDR` | — | Optional; shares events across control-plane instances |
+
+The installer generates the secrets and preserves them across upgrades —
+rotating them would invalidate every session and make stored MFA secrets
+unreadable.
+
+
+
+
+runix-agent
+
+
+
+| Variable | Default | Description |
+|---|---|---|
+| `RUNIX_AGENT_SERVER_URL` | — | Control-plane URL to dial (required) |
+| `RUNIX_AGENT_TOKEN` | — | Enrollment token, minted by the control plane (required) |
+| `RUNIX_AGENT_DATA_DIR` | `/opt/runix/agent` | Where supervised daemon state lives |
+| `RUNIX_AGENT_SHELL` | login shell | Shell used for host terminals |
+
+
+
## Releases
Each release publishes, for `linux/amd64` and `linux/arm64`:
-- `runix-server_linux_` — the control plane
-- `runix-agent_linux_` — the agent
-- `install.sh` — this installer
-- `SHA256SUMS` — checksums; the installer verifies every download against it
+| File | |
+|---|---|
+| `runix-server_linux_` | The control plane, with the web UI embedded |
+| `runix-agent_linux_` | The agent |
+| `install.sh` | The installer |
+| `SHA256SUMS` | Checksums for everything above |
-Binaries are built automatically from source on tag by the CI runner and
-published to object storage as well as to this repository. `latest.json`
-beside them names the newest version and every asset's checksum — that is
-what the installer and the in-app updater read.
+Built automatically from source by CI on every version tag, and published here
+and to object storage. `latest.json` names the newest version and each asset's
+checksum — it is what the installer and the in-app updater read, and it is
+written last so a half-finished upload is never advertised.
-## Verify a download by hand
+### Verify a download by hand
```sh
-curl -fsSLO https://runix-releases.s3-alpha-web.astra-dev.online/v0.5.0/runix-server_linux_amd64
-curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/v0.5.0/SHA256SUMS | sha256sum -c --ignore-missing
+BASE=https://runix-releases.s3-alpha-web.astra-dev.online
+curl -fsSLO $BASE/v0.5.2/runix-server_linux_amd64
+curl -fsSL $BASE/v0.5.2/SHA256SUMS | sha256sum -c --ignore-missing
```
+
+## Security notes
+
+- Agents dial **out**; nothing connects in to a managed host.
+- Every downloaded binary is verified against a published SHA-256 before it
+ replaces anything. An unverified download is refused, not warned about.
+- `/opt/runix/etc/*.env` holds the secrets and is written `0600`.
+- Access is deny-by-default: a permission can be granted globally, per server
+ group, per server, or on a single runtime, and the UI only renders controls
+ the account can actually use.
+- Runix runs workloads you define, as the user you configure. Treat access to
+ the control plane as equivalent to shell on every managed host.
diff --git a/docs/images/audit.png b/docs/images/audit.png
new file mode 100755
index 0000000..616f490
Binary files /dev/null and b/docs/images/audit.png differ
diff --git a/docs/images/console.png b/docs/images/console.png
new file mode 100755
index 0000000..0ec5d66
Binary files /dev/null and b/docs/images/console.png differ
diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png
new file mode 100755
index 0000000..1d99bad
Binary files /dev/null and b/docs/images/dashboard.png differ
diff --git a/docs/images/docker.png b/docs/images/docker.png
new file mode 100755
index 0000000..92d2c3d
Binary files /dev/null and b/docs/images/docker.png differ
diff --git a/docs/images/files.png b/docs/images/files.png
new file mode 100755
index 0000000..4643720
Binary files /dev/null and b/docs/images/files.png differ
diff --git a/docs/images/grants.png b/docs/images/grants.png
new file mode 100755
index 0000000..99ef7ee
Binary files /dev/null and b/docs/images/grants.png differ
diff --git a/docs/images/runtimes.png b/docs/images/runtimes.png
new file mode 100755
index 0000000..d913789
Binary files /dev/null and b/docs/images/runtimes.png differ
diff --git a/docs/images/schedule.png b/docs/images/schedule.png
new file mode 100755
index 0000000..f4914a8
Binary files /dev/null and b/docs/images/schedule.png differ
diff --git a/docs/images/server-overview.png b/docs/images/server-overview.png
new file mode 100755
index 0000000..76438f3
Binary files /dev/null and b/docs/images/server-overview.png differ
diff --git a/docs/images/terminal.png b/docs/images/terminal.png
new file mode 100755
index 0000000..7e4217f
Binary files /dev/null and b/docs/images/terminal.png differ