openvibers/openvibe.host Commits
Host: the product at openre.stream is OpenRestream (#43)
Changes
@@@ -2,7 +2,7 @@> The network's deployment/control plane first; then isolated hosting for community sites, bots and mods.--**Status:** alpha. Stage A (operator plane) is a CLI, `ovhost`, tested against a fake host. It is installed on the production host (2026-09-23, `/usr/local/bin/ovhost`, inventory `/etc/openvibe/host.json`) and it deploys 20 services (`ovhost deploy <svc>`: Network, Events, Chat, Billing, Tips, VIP, AI, Search, Sources, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes, Host, Media, Community; 70+ releases since 2026-09-23, with automatic rollback on a failed readiness check) and runs the nightly backups and the restore drills of 19 services. Live, Tools, Sites, Games and OpenRe moved to it in the 2026-09-27 cutover (roadmap WS-N task 11) through the deploy strategies `release-layout`, `multi-app`, `static-build` and `pnpm-build`, so every service now deploys with `sudo ovhost deploy <svc>` (Live with `--wait-idle`); each of those repositories keeps `deploy/scripts/deploy.sh` as a thin wrapper that hands over to ovhost or falls back to its old script: [docs/deploy-strategies.md](docs/deploy-strategies.md). Stage B (tenant static hosting) is a service, written and tested, and **launched on 2026-10-07**: `openvibe.host` serves the dashboard, API and front page, and `*.openvibe.host` serves tenant sites (vhosts from `ovhost nginx tenants host --install`; the Sites placeholder and the pending 404 vhost are gone; certificate lifecycle on `openvibe-certs.timer`; an nginx catch-all refuses hosts no site names). The launch record, verification and rollback: [docs/launch.md](docs/launch.md); the threat review: [docs/threat-review.md](docs/threat-review.md). Stage C (sandboxed user code) is **not started**.++**Status:** alpha. Stage A (operator plane) is a CLI, `ovhost`, tested against a fake host. It is installed on the production host (2026-09-23, `/usr/local/bin/ovhost`, inventory `/etc/openvibe/host.json`) and it deploys 20 services (`ovhost deploy <svc>`: Network, Events, Chat, Billing, Tips, VIP, AI, Search, Sources, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes, Host, Media, Community; 70+ releases since 2026-09-23, with automatic rollback on a failed readiness check) and runs the nightly backups and the restore drills of 19 services. Live, Tools, Sites, Games and OpenRestream moved to it in the 2026-09-27 cutover (roadmap WS-N task 11) through the deploy strategies `release-layout`, `multi-app`, `static-build` and `pnpm-build`, so every service now deploys with `sudo ovhost deploy <svc>` (Live with `--wait-idle`); each of those repositories keeps `deploy/scripts/deploy.sh` as a thin wrapper that hands over to ovhost or falls back to its old script: [docs/deploy-strategies.md](docs/deploy-strategies.md). Stage B (tenant static hosting) is a service, written and tested, and **launched on 2026-10-07**: `openvibe.host` serves the dashboard, API and front page, and `*.openvibe.host` serves tenant sites (vhosts from `ovhost nginx tenants host --install`; the Sites placeholder and the pending 404 vhost are gone; certificate lifecycle on `openvibe-certs.timer`; an nginx catch-all refuses hosts no site names). The launch record, verification and rollback: [docs/launch.md](docs/launch.md); the threat review: [docs/threat-review.md](docs/threat-review.md). Stage C (sandboxed user code) is **not started**.**Domain:** `openvibe.host` (dashboard and API) and `*.openvibe.host` (tenant sites). Launched on 2026-10-07 (see [Launch rule](#launch-rule)); OpenVibe.Sites no longer serves it.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.3 and §15.17; roadmap Wave 21.**License:** AGPL-3.0 (same as every OpenVibe service).@@@ -20,7 +20,7 @@ Every repository ships its own deploy script, and each one encodes rules learned| Install only when the lockfile or dependencies changed; restore `package-lock.json` afterwards | Network `deploy.sh` and host practice | `npm install --omit=dev` as the checkout owner, then `git checkout -- package-lock.json` if npm rewrote it. || Every dependency must resolve before anything restarts | Tools `deploy.sh` (an emptied `file:` link crash-looped every unit) | `node_modules/<dep>/package.json` must exist, parse and name the package. A broken dependency is reinstalled once, and if it still fails the deploy aborts and the checkout is restored. || Build, install vhosts, `nginx -t`, reload | Sites `deploy.sh` | `build` hooks and `nginx.installOnDeploy`. Vhost installs are transactional: if `nginx -t` fails, the previous files come back and nginx is not reloaded. |--| Build a release while the old one serves; switch a symlink; roll back to a release with its own `node_modules`; pid 1 must hold the socket | Live `deploy.sh` (release layout), OpenRe `deploy.sh release`/`api` | Strategy `release-layout` ([docs/deploy-strategies.md](docs/deploy-strategies.md)). The socket unit is restarted only when its unit file changed or systemd does not hold the port. |++| Build a release while the old one serves; switch a symlink; roll back to a release with its own `node_modules`; pid 1 must hold the socket | Live `deploy.sh` (release layout), OpenRestream `deploy.sh release`/`api` | Strategy `release-layout` ([docs/deploy-strategies.md](docs/deploy-strategies.md)). The socket unit is restarted only when its unit file changed or systemd does not hold the port. || Per-app installs (not `apps/_shared`), jobs runtime and guard load before the restart, every `openvibe-tools*` unit | Tools `deploy.sh` | Strategy `multi-app`: `skipPackages`, `preflight`, `unitsMatch`, `ready.release`. || git as the checkout owner; never leave root-owned files under `/opt` | Community/Events/Tools checkouts | git, npm, builds, SQLite queries and backups all run as the declared owner/service user (`runuser` from root, `sudo -u` otherwise); PostgreSQL dumps and probes run as the postgres OS user. |@@@ -150,7 +150,7 @@ Every command accepts `--json` and `--inventory <file>`. `drill` exits `0` passe`service add` runs as root against `/etc/openvibe/host.json`. It copies `services.<id>` from the installed CLI's `host.example.json`, drops `_note`, and backs up the live inventory before changing it. The example entry may set `cloneUrl` to a credential-free GitHub HTTPS URL; otherwise the repository is `https://github.com/OpenVibers/OpenVibe.<Capitalized id>.git`. The command refuses an existing entry, occupied checkout or env file. It holds the inventory lock (`<stateDir>/locks/_inventory.lock`) from reading the inventory to the end, and refuses a symlinked `.env.example`, which it reads with the checkout owner's permissions. It clones as the checkout owner, creates the entry's `envFile` (`/etc/openvibe/<id>.env`, 0600) as a copy of the checkout's `.env.example` with production `NODE_ENV`, `HOST` and `PORT`. It sets `BASE_URL` when the inventory vhost, origin or env example supplies a public HTTPS origin; otherwise it leaves an existing empty `BASE_URL` for the operator and does not add one to services without it. An explicitly required `BASE_URL` needs an inventory vhost or origin before setup begins. The tracked `.env.example` is never changed. It runs `data provision` if that example declares `DATABASE_URL`. Output lists env names but never values. `--dry-run` shows the plan without changing files. It then lists the env names still empty (leaving out the production overrides and, after provisioning, `DATABASE_URL`, `DATABASE_DIRECT_URL`, `VALKEY_URL`, `VALKEY_PREFIX`). When the example declares `OV_OAUTH_CLIENT_ID` or `OV_OAUTH_CLIENT_SECRET`, it prints one root command for the Network service principal. The command runs `server/setup/service-principal.js` through `systemd-run` with Network's env file (`services.network.envFile`, default `/etc/openvibe/network.env`) in Network's checkout (`services.network.repo`, default `/opt/openvibe.network`). It uses `rotate` when `list` already shows the id and `create` otherwise, so the principal never lands in a development database (the secret is never printed). It ends with `then: ovhost validate <id>; ovhost deploy <id> --restart`; ovhost runs neither. If a later step fails, the inventory backup path is printed; completed setup steps are not automatically undone.--**Deploy strategies** (roadmap WS-N task 11). The inventory entry's `strategy` picks how a service deploys: `git-checkout` (the default: an in-place checkout, every service above), `multi-app` (Tools), `static-build` (Sites), `pnpm-build` (Games) or `release-layout` (Live, OpenRe: `releases/<id>` behind a `current` symlink, prepared while the old release serves, `--prepare-only` to stop after preparing). Each strategy carries the rules of the script it replaces: preflight checks, build output restored from git, untracked lockfiles, `unitsMatch`, `/release.json` naming the new sha, the socket rule. [docs/deploy-strategies.md](docs/deploy-strategies.md) has the engines, every inventory field, the rule-by-rule mapping from each script, what was not ported and why, the production cutover checklist and the proposed inventory diff.++**Deploy strategies** (roadmap WS-N task 11). The inventory entry's `strategy` picks how a service deploys: `git-checkout` (the default: an in-place checkout, every service above), `multi-app` (Tools), `static-build` (Sites), `pnpm-build` (Games) or `release-layout` (Live, OpenRestream: `releases/<id>` behind a `current` symlink, prepared while the old release serves, `--prepare-only` to stop after preparing). Each strategy carries the rules of the script it replaces: preflight checks, build output restored from git, untracked lockfiles, `unitsMatch`, `/release.json` naming the new sha, the socket rule. [docs/deploy-strategies.md](docs/deploy-strategies.md) has the engines, every inventory field, the rule-by-rule mapping from each script, what was not ported and why, the production cutover checklist and the proposed inventory diff.`deploy --browser-check` checks the service's public site in headless Chrome after a deploy that went through. It runs [`scripts/browser-check.js`](docs/browser-check.md) for that one site. The check is report only: it never changes the exit code or the release record. It needs Chrome where ovhost runs.@@@ -210,7 +210,7 @@ The sandbox blocks writes outside the drill directory and addresses beyond loopb### Inventory--`host.example.json` describes every service on the host. It records the checkout, owner/`runAs`, units and socket unit, `unitSources` (repo unit files), env file, `env.required` (names, or `"from-example"`), port, loopback `ready` URL (plus headers, since Tools needs `Host: openvibe.tools`), `packages` (`"apps/*"` for Tools), install command, `build` hooks, `noRestartPaths`, the `protected` probe (`http-json-count`, `sqlite-count`, `postgresql-count`, or `sum` of several: Live streams, Media recordings, Events SSE connections, Chat sockets, Games players online, OpenRe ingest sessions, Tools running jobs), the `drain` policy, `databases` (each a SQLite file `{ "name", "path" }` or a PostgreSQL database `{ "name", "engine": "postgresql", "database": "ov_<name>" }`; [docs/db-inventory.md](docs/db-inventory.md)), `backupOnChange`, `nginx`, `layout` (`release` for a `releases/<id>` + `current` service deployed by its own script, which ovhost does not manage) and `workerUnits`. `workerUnits` are units, or templates such as OpenRe's `[email protected]`, that belong to the service but that ovhost **never starts, stops or restarts**: they drain and exit on their own. `status` and `validate` list their instances read-only, `validate` warns when none runs, and the inventory refuses a worker unit, or an instance of one, in `units`. The real file is `/etc/openvibe/host.json` and is **never committed**. When `ovhost` runs as root it refuses an inventory that is not root-owned or that anyone else can write, and it rejects relative paths, non-loopback probe URLs, non-SELECT probe queries and vhost names that contain a path.++`host.example.json` describes every service on the host. It records the checkout, owner/`runAs`, units and socket unit, `unitSources` (repo unit files), env file, `env.required` (names, or `"from-example"`), port, loopback `ready` URL (plus headers, since Tools needs `Host: openvibe.tools`), `packages` (`"apps/*"` for Tools), install command, `build` hooks, `noRestartPaths`, the `protected` probe (`http-json-count`, `sqlite-count`, `postgresql-count`, or `sum` of several: Live streams, Media recordings, Events SSE connections, Chat sockets, Games players online, OpenRestream ingest sessions, Tools running jobs), the `drain` policy, `databases` (each a SQLite file `{ "name", "path" }` or a PostgreSQL database `{ "name", "engine": "postgresql", "database": "ov_<name>" }`; [docs/db-inventory.md](docs/db-inventory.md)), `backupOnChange`, `nginx`, `layout` (`release` for a `releases/<id>` + `current` service deployed by its own script, which ovhost does not manage) and `workerUnits`. `workerUnits` are units, or templates such as OpenRestream's `[email protected]`, that belong to the service but that ovhost **never starts, stops or restarts**: they drain and exit on their own. `status` and `validate` list their instances read-only, `validate` warns when none runs, and the inventory refuses a worker unit, or an instance of one, in `units`. The real file is `/etc/openvibe/host.json` and is **never committed**. When `ovhost` runs as root it refuses an inventory that is not root-owned or that anyone else can write, and it rejects relative paths, non-loopback probe URLs, non-SELECT probe queries and vhost names that contain a path.**Lifecycle** (roadmap WS-P task 1, `lib/lifecycle.js`). Each service's lifecycle is the `lifecycle` block of its manifest in openvibe-contracts (≥ 0.55.0): liveness, shutdown (signal, `deadlineSeconds`, drains, workers), startupRecovery, rollback, contracts and leases. A service entry may carry its own `lifecycle` block, which replaces the manifest's on this host, and `validate --manifest <file>` reads another manifest file. `ovhost validate` fails when no block is found or a field is missing, naming the service and the field (`live: lifecycle.shutdown.deadlineSeconds is missing`). It also fails when `deadlineSeconds` exceeds a unit's stop timeout (systemd's effective `TimeoutStopUSec`, else `TimeoutStopSec` in the unit source) or the unit's `KillSignal` is another signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`. Worker units that drain longer than their stop timeout are a warning, since ovhost never stops them. This repository still pins openvibe-contracts v0.49.0, which has no lifecycle blocks, so until that pin moves to v0.55.0 or later `validate` reports every service as undeclared.@@@ -39,7 +39,7 @@"host.release.published"],"stages": {-- "A": "alpha: ovhost CLI installed on the production host 2026-09-23 (/usr/local/bin/ovhost, inventory /etc/openvibe/host.json); every service deploys with `sudo ovhost deploy <svc>` since the 2026-09-27 cutover (roadmap WS-N task 11: Live, Tools, Sites, Games and OpenRe through the release-layout, multi-app, static-build and pnpm-build strategies); nightly backups, restore drills (19 passed 2026-09-23, docs/restore-drills.md), release notifications, incidents and maintenance, the alerts relay; the D41 deploy-and-rollback proof (scripts/d41-proof.sh) is written and tested",++ "A": "alpha: ovhost CLI installed on the production host 2026-09-23 (/usr/local/bin/ovhost, inventory /etc/openvibe/host.json); every service deploys with `sudo ovhost deploy <svc>` since the 2026-09-27 cutover (roadmap WS-N task 11: Live, Tools, Sites, Games and OpenRestream through the release-layout, multi-app, static-build and pnpm-build strategies); nightly backups, restore drills (19 passed 2026-09-23, docs/restore-drills.md), release notifications, incidents and maintenance, the alerts relay; the D41 deploy-and-rollback proof (scripts/d41-proof.sh) is written and tested","B": "launched 2026-10-07 at openvibe.host (dashboard, API, front page) and *.openvibe.host (tenant sites); docs/launch.md, threat review in docs/threat-review.md","C": "not started (deliberately: blocked until isolation and metering are proven)"},@@@ -28,7 +28,7 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat| Status on 2026-09-23 | Entries ||---|---|| Cut over; the shim is now only a rollback lever | C-01–C-03 (chat), C-10–C-13 (pastes), C-20–C-21 (AI) |--| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRe) |++| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRestream) || Running in parallel, waiting on telemetry | C-60–C-61 (Events v1 signatures) || Legacy secret or credential still accepted | C-42, C-70, C-71 (Media app keys) || Legacy data kept for rollback | C-72 (analytics backups with IPs), C-73 (frozen Live tables) |@@@ -75,13 +75,13 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat| C-31 | **`money_writes_frozen`** (site setting, owner-only, both modes), with `/api/admin/money/freeze`. | Live `server/monetization/money-authority.js:54-111`, `server/admin/routes.js:492-517` | Live | A permanent incident control, not a shim. It stays. | kept | It is itself the lever. || C-32 | **Live's provider webhooks** `/api/powerchat/webhook` and `/api/payments/webhook/*`. They answer 410 under `billing`. | Live `server/monetization/payments-routes.js:15-25` | Live | *proposed*: removed with C-30's write paths. A 410 makes PowerChat disable the endpoint (Billing `docs/live-cutover.md:170-175`), so remove them only after PowerChat points at Billing. | cutover + 30 days | Re-point the webhook back to Live. |--## Stream ingest (OpenRe deployed; no slot switched)++## Stream ingest (OpenRestream deployed; no slot switched)| # | What | Where | Owner | Removal condition | Target | Rollback lever until then ||---|---|---|---|---|---|---|--| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRe | *documented*: "Live's ingest code stays until the last protocol has run on OpenRe for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRe `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRe `README.md:205`). |--| C-41 | **OpenRe port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRe `server/config.js:24,72`, `README.md:139-145` | OpenRe | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |--| C-42 | **OpenRe `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRe `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRe + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |++| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRestream | *documented*: "Live's ingest code stays until the last protocol has run on OpenRestream for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRestream `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRestream `README.md:205`). |++| C-41 | **OpenRestream port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRestream `server/config.js:24,72`, `README.md:139-145` | OpenRestream | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |++| C-42 | **OpenRestream `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRestream `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRestream + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |## The shared internal key (`INTERNAL_API_KEY` / `X-Internal-Key`): retired 2026-09-29@@@ -157,7 +157,7 @@ These match the search terms but are permanent. They are listed so the next revi- **Network `identity_legacy_map`** and `POST /internal/identity/legacy-map`: ADR-001 keeps legacy ids alongside subject ids permanently (Network `server/identity/subjects.js:25-35`). The *key* on that route is C-50.- **Network legacy public-key shape**: `public_key` PEM is served beside the spec shape (`server/index.js:371-386`). Billing, Blog, Chat, Codes, Community, Coupons, AI, Host, Media and Events read `public_key`. Removing it would be a breaking change for all of them.- **Network `my.openvibe.network`**: 301 to the apex (`server/index.js:571`).--- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRe use their own handlers.++- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRestream use their own handlers.- **Analytics `ip`/`user_id`/`city` columns** kept and always NULL: Network `server/analytics/schema.js:1-14`, Tools `apps/_shared/analytics/schema.js:3-6`.- **Billing `legacy_order_id`/`legacy_live_txn`/`legacy_live_id`**: import provenance (`server/db.js:120,142,162`). Tips `legacy_source` and VIP `vip_migration_maps` are the same kind of provenance.- **Tools registry fallback list** until Network answers (`registry/services.js:16-24`), and `isLegacyHost` for the old paste/login hosts (`index.js:104-105`).@@@ -29,13 +29,13 @@ State markers:| C | AI → OpenVibe.AI (Wave 13) | **DONE** 2026-09-23 ~15:40 UTC (`AI_SERVICE=remote`) | AI [`docs/migration.md`](https://github.com/OpenVibers/OpenVibe.AI/blob/main/docs/migration.md) | Unset `AI_SERVICE`, restart Live || D | Events delivery signature v2 | **IN PROGRESS**: Events sends v2; consumer deploys unknown; v1 still sent | Events [`docs/replay-window-rollout.md`](https://github.com/OpenVibers/OpenVibe.Events/blob/main/docs/replay-window-rollout.md) | The consumer redeploys its previous release; replay the DLQ || E | Money → OpenVibe.Billing | **BLOCKED (owner)**: PowerChat dashboard. Billing runs in shadow. | Billing [`docs/live-cutover.md`](https://github.com/OpenVibers/OpenVibe.Billing/blob/main/docs/live-cutover.md) | Depends on the step reached. After step 9 there is **no tool** |--| F | RTMP ingest → OpenRe.Stream | **BLOCKED (owner)**: broadcaster windows and the per-slot rehearsal. DNS `ingest.openre.stream` is done, port 1936 is open (2026-09-23) and OpenRe is deployed. | OpenRe [`docs/cutover.md`](https://github.com/OpenVibers/OpenRe.Stream/blob/main/docs/cutover.md) (steps, checks and rollback; `scripts/cutover-preflight.js`) | Per slot `{"authority":"live"}` + `set-definition-state.js --state disabled`. All slots: unset `OPENRE_URL` |++| F | RTMP ingest → OpenRestream | **BLOCKED (owner)**: broadcaster windows and the per-slot rehearsal. DNS `ingest.openre.stream` is done, port 1936 is open (2026-09-23) and OpenRestream is deployed. | OpenRestream [`docs/cutover.md`](https://github.com/OpenVibers/OpenRestream/blob/main/docs/cutover.md) (steps, checks and rollback; `scripts/cutover-preflight.js`) | Per slot `{"authority":"live"}` + `set-definition-state.js --state disabled`. All slots: unset `OPENRE_URL` || G | Retire the shims | **NOT STARTED**. Dated in [compatibility-register.md](compatibility-register.md) | Network [`docs/retirement.md`](https://github.com/OpenVibers/OpenVibe.Network/blob/main/docs/retirement.md) | per entry |The remaining order is D → E → F → G.- D first: nothing depends on it, and v1 can stop only when every consumer requires v2.- E before F: the Billing switch restarts Live once, and it is best done before any stream depends on-- OpenRe.++ OpenRestream.- G follows each entry's date in the register.## What the owner must do@@@ -52,9 +52,9 @@ These actions need the owner's accounts or physical presence. Nothing in this rufirst, and Billing is frozen to hold deliveries. A 410 from Live makes PowerChat disable the endpoint(Billing `docs/live-cutover.md:131-134, 170-175`).2. **Cloudflare DNS, for step F.** Create an A record `ingest.openre.stream` pointing at the host's IP,-- **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRe `README.md:145`,++ **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRestream `README.md:145`,`deploy/nginx/openre.stream.conf:15-17`).--3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRe++3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRestream`README.md:145,160`). *Done 2026-09-23: 1936 answers from outside since 19:24 UTC.*3a. **DNS changes in a cutover (any step that moves a hostname).** A day before: lower the record's TTL to 60 sand write down the current records (`dig +noall +answer <name> A AAAA CNAME` into the step's notes). At the@@@ -117,11 +117,11 @@ These rules are learned from outages. `ovhost` encodes them (see [README](../REA(`deploy/scripts/deploy.sh:1-42`.) It has **no automatic rollback** and does not check `/api/ready`.`apps/gateway/deploy/scripts/deploy.sh` is the superseded per-app script. Do not use it.--- **OpenRe.Stream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.++- **OpenRestream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.- `api` restarts the API and coordinator only, polls `/api/ready` for 60 s, and rolls back on failure(exit 2).- `workers` starts a new generation. The old ones drain for up to 30 min.-- - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRe `README.md:147-160`.)++ - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRestream `README.md:147-160`.)- **Everything else** (Network, Community, Events, Chat, Billing, and the loopback services): pull,install if the lockfile changed, restart the unit, then poll `/api/ready`. Network's own`deploy.sh` and Sites' `deploy.sh` exist (Host `README.md`, rules table).@@@ -316,9 +316,9 @@ PowerChat delivery lands in exactly one ledger.Read the full list before choosing the day. VIP checkout stays closed until this cutover.--## F. RTMP ingest → OpenRe.Stream: BLOCKED on the owner (rehearsal and broadcaster windows; DNS and port 1936 are done)++## F. RTMP ingest → OpenRestream: BLOCKED on the owner (rehearsal and broadcaster windows; DNS and port 1936 are done)--OpenRe is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are++OpenRestream is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are`openre-session-coordinator`, `openre-rtmp-ingest@<sha>` and `openre-restream-worker@<sha>`. The databaseis `/var/lib/openre/openre.db`. RTMP is bound to 127.0.0.1:1936 (`OPENRE_RTMP_BIND`). A loopback rehearsalwith an ffmpeg test pattern passed: the session went live→ended, a wrong key was refused, `openre.session.*`@@@ -326,14 +326,14 @@ reached Events, and the key never appeared in logs.Live carries the integration (`f0ca18b`). It is inert until `OPENRE_URL` is set. `ingest_authority`defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP moves--(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRe `README.md:207-209`).++(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRestream `README.md:207-209`).--**Once, before the first slot.** OpenRe's [`docs/cutover.md`](https://github.com/OpenVibers/OpenRe.Stream/blob/main/docs/cutover.md) supersedes the table below: it has the exact commands, the owner and agent steps (A1–A7), and a read-only preflight whose output on 2026-09-23 settles the "Unknown" rows (no subscription, no `OPENRE_*` in `live.env`).++**Once, before the first slot.** OpenRestream's [`docs/cutover.md`](https://github.com/OpenVibers/OpenRestream/blob/main/docs/cutover.md) supersedes the table below: it has the exact commands, the owner and agent steps (A1–A7), and a read-only preflight whose output on 2026-09-23 settles the "Unknown" rows (no subscription, no `OPENRE_*` in `live.env`).| Step | State ||---|---|| 1. Network client `openre` and its grants; release the contract proposals | **DONE** (operator notes: principal `openre` in `openre.env`; openre capabilities released by contracts v0.19.0) |--| 2. Deploy OpenRe; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |++| 2. Deploy OpenRestream; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** || — Owner: DNS-only `ingest.openre.stream`; 1936/tcp open at the host and provider edge; RTMP bound publicly (`OPENRE_RTMP_BIND`) | **BLOCKED (owner)** || 3. Events subscription `openre.session.*` → `http://127.0.0.1:3000/internal/openre-events`; its secret in Live as `OPENRE_EVENTS_SECRET` | Unknown. Check `GET /api/v1/subscriptions` on Events and `ovhost validate live`. || 4. Live: `OPENRE_URL=http://127.0.0.1:4500`, `OPENRE_PUBLIC_URL`, then `deploy.sh --wait-idle`. With no slot switched this changes nothing. | Unknown. Check `GET /api/admin/openre/status`. |@@@ -356,8 +356,8 @@ defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMPLive RTMP URL.- All slots: unset `OPENRE_URL` in `live.env` and restart Live (`README.md:205`,`server/openre/authority.js:47-49`).--- Live's ingest code stays until the last protocol has run on OpenRe for two weeks (ADR-009 §Rollback).--- Port 1935 moves to OpenRe (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening++- Live's ingest code stays until the last protocol has run on OpenRestream for two weeks (ADR-009 §Rollback).++- Port 1935 moves to OpenRestream (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening(`README.md:139-145`).## G. Retiring the shims: NOT STARTED@@@ -56,7 +56,7 @@ For each of these units, `systemctl show` records `MainPID`, `InvocationID`, `Ac`ExecMainStartTimestamp`, `NRestarts`, `ActiveState` and `SubState`:- every unit and socket in the inventory except sources' own unit;--- every `openvibe-*` and `openre-*` unit (for example OpenRe's transport worker instances);++- every `openvibe-*` and `openre-*` unit (for example OpenRestream's transport worker instances);- `nginx.service`;- always, even if not loaded: `openvibe-live.socket`, `openvibe-live.service`,`openvibe-chat.service` and `openvibe-media.service`;@@@ -24,7 +24,7 @@ root (`sudo ovhost data …`; `--dry-run` need not): the scripts need it, and an`VALKEY_PREFIX` — never a value.- **`data switch`** runs `roles/data/switch-service.sh <service> [<file>]`. It takes `SWITCH_UNITS` and`SWITCH_DIR` from the inventory entry (`units` plus `workerUnits`, a template worker shown as a glob-- such as `openre-rtmp-ingest@*.service`, plus the `unitsMatch` glob when set; `repo`), so a release-layout service such as OpenRe needs no++ such as `openre-rtmp-ingest@*.service`, plus the `unitsMatch` glob when set; `repo`), so a release-layout service such as OpenRestream needs nohand-set environment. `--sqlite <file>` names the SQLite file when it is not`/var/lib/openvibe-<service>/<service>.db` (Host: `/var/lib/openvibe-host-api/host.db`). Before runningit, the service's postgres branch must be merged with green CI and its auto-deploy frozen@@@ -38,7 +38,7 @@ logical `.dump` feeds the drills ([backups.md](backups.md#postgresql-services)).keeps its string form, and PostgreSQL is the object `{ "engine": "postgresql", "database": "ov_host" }`.- the `protected` probe kind `postgresql-count` — `{ "kind": "postgresql-count", "database": "ov_openre","sql": "SELECT count(*) …", "label": "…" }`: a read-only SELECT against a database in the cluster, run as-- the postgres OS user. OpenRe's protected probe (its active ingest sessions) is one, on `ov_openre`. The++ the postgres OS user. OpenRestream's protected probe (its active ingest sessions) is one, on `ov_openre`. Theother kinds are `http-json-count`, `sqlite-count` and `sum` ([lib/probes.js](../lib/probes.js)).---@@@ -8,7 +8,7 @@ Each proof is a recorded production run: what was deployed, how it was observed,| Recorder / media-worker checkpoint (Media) | open | needs a live ingest during a Media deploy || Chat resume (Chat) | passed 2026-09-26, with a limit | [below](#chat-resume-chat-2026-09-26) || Game shard (Games) | passed 2026-09-26 | [below](#game-shard-games-2026-09-26) |--| Ingest worker (OpenRe) | after the cutover (WS-H) | |++| Ingest worker (OpenRestream) | after the cutover (WS-H) | |The release-lifecycle acceptance suite ([release-acceptance.md](release-acceptance.md)) reads this table. The state of a row and the numbers quoted from its record are gates `6f`, `6g`, `6h` and `7e`: a row that says `passed` must still contain those numbers, and an `open` row stays open there.@@@ -1,7 +1,7 @@# Deploy strategies (roadmap WS-N task 11)`ovhost deploy <svc>` now covers the services that deployed with their own `deploy/scripts/deploy.sh`:--Live (release layout), Tools, Sites, OpenRe and Network, and Games (which deployed by hand). Each service++Live (release layout), Tools, Sites, OpenRestream and Network, and Games (which deployed by hand). Each serviceentry in the inventory names a **strategy**: an engine plus the defaults the script it replaces encoded.Every repository keeps `deploy/scripts/deploy.sh` as a thin wrapper that maps its old flags onto ovhost,with the old script body kept as `deploy/scripts/deploy-legacy.sh` for the fallback.@@@ -15,7 +15,7 @@ reached where an ovhost without `capabilities` answers, which no longer happens| Service | Strategy | Replaces | Wrapper (`deploy/scripts/deploy.sh`) ||---|---|---|---|| live | `release-layout` | Live `deploy.sh` (release layout) | `--wait-idle`, `--restart`, `--rollback` → `ovhost rollback live`, `DRY_RUN=1` → `ovhost plan live`; `--force` refused (see below) |--| openre | `release-layout` | OpenRe `deploy.sh release` + `api` | `deploy`, `release [<ref>]` → `--prepare-only`, `api [<sha>]` → `deploy --to <sha>`, `rollback`, `plan`; `workers`, `status`, `prune` stay in the legacy script |++| openre | `release-layout` | OpenRestream `deploy.sh release` + `api` | `deploy`, `release [<ref>]` → `--prepare-only`, `api [<sha>]` → `deploy --to <sha>`, `rollback`, `plan`; `workers`, `status`, `prune` stay in the legacy script || tools | `multi-app` | Tools `deploy.sh` | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` || sites | `static-build` | Sites `deploy.sh` (after a manual pull) | always `ovhost deploy sites --restart` (rebuild every run, as before); `DRY_RUN=1` || games | `pnpm-build` | the manual procedure | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |@@@ -47,7 +47,7 @@ original arguments. So an old ovhost, a missing one, or an inventory that has nobreaks a deploy. `OVHOST_LEGACY=1` forces the fallback; `OVHOST=<path>` picks another ovhost. Where the legacyscript cannot do what was asked (Tools, Network and Games have no `--rollback`; none of them has `DRY_RUN`except Live), the wrapper refuses instead of doing something else. The Sites fallback pulls first (it restores--`dist/` and runs `git pull --ff-only` as the checkout owner), then runs the pulled legacy script. OpenRe's++`dist/` and runs `git pull --ff-only` as the checkout owner), then runs the pulled legacy script. OpenRestream'sfallback runs `release` then `api` for `deploy`.Exit codes are ovhost's: `0` ok · `1` usage/precondition · `2` validation failed, nothing restarted · `3` not@@@ -103,7 +103,7 @@ mean. (In the fallback, `--force` goes to the legacy script as before.)reasons). After readiness a listener that still is not systemd's is reported (`✗ socket activation is notin effect`) and recorded (`socketHeld: false`).6. Prune to `release.keep`: never `current`, never the release just left, never one a worker unit instance-- runs from (OpenRe's `openre-rtmp-ingest@<id>`).++ runs from (OpenRestream's `openre-rtmp-ingest@<id>`).7. Record (`fromRelease`, `toRelease`), announce.`ovhost rollback <svc>` switches back to the release the release log says the current one replaced (or@@@ -120,10 +120,10 @@ overrides them.| `strategy` | `git-checkout`, `multi-app`, `static-build`, `pnpm-build` or `release-layout` | `git-checkout`; an entry with `layout: "release"` and no strategy stays unmanaged (deployed by its own script), as before || `release.git` | the clone releases are made from (relative to `repo`, or absolute inside it) | `repo` || `release.releases`, `release.current` | the releases directory and the link | `releases`, `current` |--| `release.id` | `time-sha8` (`<UTC yyyymmdd-HHMMSS>-<sha8>`, Live; the old script used the host's local time, the same on a UTC host) or `sha12` (OpenRe; one release per commit, reused) | `time-sha8` |++| `release.id` | `time-sha8` (`<UTC yyyymmdd-HHMMSS>-<sha8>`, Live; the old script used the host's local time, the same on a UTC host) or `sha12` (OpenRestream; one release per commit, reused) | `time-sha8` || `release.links` | `{ name: target }` links made in every release | `{}` || `release.reuseModules` | hard-link the current `node_modules` when dependencies did not change | `true` |--| `release.chown` | `user[:group]` for each new release (OpenRe) | none |++| `release.chown` | `user[:group]` for each new release (OpenRestream) | none || `release.keep` | releases kept (2–50) | 5 || `release.settleSeconds` | wait after a switch without restart before the ready check | 3 || `release.socketPort` | the port pid 1 must hold | the service `port` |@@@ -172,13 +172,13 @@ itself (`/opt/openvibe.live/current`) is read the same way. `status`, `validate`| Sites: pull BEFORE running | ovhost fetches and merges first by construction || Sites: `npm ci`, `node build.js`, vhosts, `nginx -t` then reload, never reload after a failed test | static-build defaults; `nginx.install` restores the previous files || Sites: one announce per placeholder, stop at the first failure | `announce.releaseFiles` |--| OpenRe: `release` (worktree, `npm ci`, chown ubuntu), `api` (switch, restart API + coordinator, 60 s ready, roll back) | release-layout with `release.id: sha12`, `reuseModules: false`, `chown`; `--prepare-only` |--| OpenRe: never touch a worker; `prune` keeps what runs | `workerUnits` are never restarted; pruning skips releases a worker instance runs from |++| OpenRestream: `release` (worktree, `npm ci`, chown ubuntu), `api` (switch, restart API + coordinator, 60 s ready, roll back) | release-layout with `release.id: sha12`, `reuseModules: false`, `chown`; `--prepare-only` |++| OpenRestream: never touch a worker; `prune` keeps what runs | `workerUnits` are never restarted; pruning skips releases a worker instance runs from || Network: pull, `npm install` on a lockfile change, unit file installed when it differs, restart, health | git-checkout with `installUnits` || Games: `git pull`, `pnpm install --frozen-lockfile`, `pnpm build`, restart | pnpm-build (git as the owner, not `sudo git`) |Stricter than the scripts, on purpose: Tools and Network roll back automatically and check every dependency;--Tools checks `/api/ready` (not `/api/health`) and the new sha; OpenRe refuses an API restart while an ingest++Tools checks `/api/ready` (not `/api/health`) and the new sha; OpenRestream refuses an API restart while an ingestsession is open (the script did not check; `--wait-idle` holds, `--force` goes ahead; if the restart provablynever cuts a session, set its `drain.policy` to `report`); `--wait-idle` now also holds a `report`-policyservice (Tools jobs, Games players, Events SSE) instead of being ignored; every attempt is in the release@@@ -194,7 +194,7 @@ log and a freeze refuses.2026-09-24; an in-place Live would be `strategy: git-checkout`.- **Live `--force`** (discard local tracked changes): ovhost never discards changes (see above).- **Network's "restart anyway" when nothing is new**: only with `--restart` now.--- **OpenRe `workers`, `status`, `prune`**: they stay in the repository's script. Starting a worker++- **OpenRestream `workers`, `status`, `prune`**: they stay in the repository's script. Starting a workergeneration is not a deploy ovhost makes (it never starts, stops or restarts a worker unit); ovhost pruneson its own and keeps every release a worker runs from.- **Games `sudo git`**: ovhost runs git as the checkout owner. If earlier `sudo git pull`s left root-owned@@@ -238,7 +238,7 @@ read-only until step 5.entry only gains `installUnits`).5. **Pull the wrappers.** Tools, Sites, Network and Games get theirs with their next pull (the old Tools andNetwork scripts pull themselves, so the deploy that brings the wrapper is still run by the old script).-- Live's arrives in the release that contains it; OpenRe's with `git -C /opt/openre.stream/repo pull`.++ Live's arrives in the release that contains it; OpenRestream's with `git -C /opt/openre.stream/repo pull`.6. **Deploy through the wrapper**, one service at a time, lowest risk first: network, sites, games (quiethour or `--wait-idle`), tools, openre (no ingest session, or `--wait-idle`), live (`--wait-idle`). Eachwrapper prints `ovhost deploy <svc> …`; if it prints "running deploy-legacy.sh", read why.@@@ -9,7 +9,7 @@ Public listeners on 2026-09-26, all intended:| 22 | tcp | sshd || 80, 443 | tcp | nginx || 1935 | tcp | Live RTMP ingest (DNS-only `ingest.openvibe.live`) |--| 1936 | tcp | OpenRe RTMP ingest |++| 1936 | tcp | OpenRestream RTMP ingest || 3478, 5349 | tcp + udp | coturn || 49152–65535 | udp | coturn relay range (`min-port`/`max-port`) || 9710–9789 | tcp | nginx JSMPEG ports (bound to the public address) |@@@ -133,7 +133,7 @@ The tab's EventSource is subject to the site's Content-Security-Policy, so `conn`https://openvibe.events`. As of 2026-09-26, every site whose pages load release-watch under a CSPallows it: Live and Community first, then, with their 1.17.1 pins, Network, Wiki, Blog, Codes, Search, Reviews,VIP, Tips, Host (Stage B dashboard), News, Deals, Coupons, Trade and the Tools apps. Chat, Media, Games,--OpenRe.Stream and Sites send no CSP with those pages. A new site needs it too. Without it, the++OpenRestream and Sites send no CSP with those pages. A new site needs it too. Without it, thebrowser refuses the stream and logs one CSP error per page load (the browser check counts it).release-watch then stops, with state `blocked`, and keeps polling. Add the origin in the same change thatmoves the pin, or set `data-events="off"` on the `ov-release` meta tag.@@@ -202,13 +202,13 @@ Supported with declared overrides (the table above records which have passed on- coupons- trade- codes--- openre (not run yet). Only openre-api starts (`drill.command`), with `OPENRE_DRILL=1`. From OpenRe++- openre (not run yet). Only openre-api starts (`drill.command`), with `OPENRE_DRILL=1`. From OpenRestream`5e86ea7` on, that makes it read-only: writes, `/play/` and sign-in answer 503. The coordinatorand every transport worker refuse to start in a drill, and the event relay and Media calls areoff. The API never runs a transport or the relay in any case. Empty `EVENTS_URL` and`OV_OAUTH_CLIENT_SECRET`, plus `MEDIA_URL` on the closed port, cover older releases. The workersstay out of every ovhost command: they are `workerUnits`, which are listed and never restarted.-- Run it after the OpenRe release with `OPENRE_DRILL` is deployed (OpenRe `docs/cutover.md`++ Run it after the OpenRestream release with `OPENRE_DRILL` is deployed (OpenRestream `docs/cutover.md`phase C).- live (not run yet). Needs OpenVibe.Live `8a58aea` or later; `drill.requires` refuses an oldercheckout. The production unit's ExecStart starts with `LIVE_DRILL=1`, `DB_PATH` on the copy and@@@ -82,10 +82,10 @@ Sources:- Games `/healthz` (the inventory now uses `/api/ready`, 2026-09-23; Network, Media and Tools too).The inventory should point at `/api/ready` where one exists. Games has none.--4. *(Fixed: the inventory has had AI and OpenRe entries since `f0dbd0c`, and both are in the scheduled++4. *(Fixed: the inventory has had AI and OpenRestream entries since `f0dbd0c`, and both are in the scheduled`ovhost backup --all` with off-host copies, [backups.md](backups.md). Their drills are still unsupported.)***Two services have no ovhost inventory entry, so no `ovhost backup` and no drill**: AI (`ai.db`) and-- OpenRe.Stream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron++ OpenRestream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cronand timers.5. **The inventory's Tools backup list is incomplete.** It names the seven `analytics.db` files only.The job databases `apps/{img,audio,docs}/data/jobs.db` (table `tool_jobs`,@@@ -132,7 +132,7 @@ reviewed on 2026-09-23.| **Community** | `/var/lib/openvibe-community/community.db` (`StateDirectory`, `deploy/systemd/openvibe-community.service:26-27`). `/opt/openvibe.community/data/community.db` is the legacy location, and the inventory keeps whichever of the two exists. `COMMUNITY_DB_PATH`, default `./data/community.db` (`server/config.js:56`). | Yes: pastes (`PASTES_AUTHORITY=community` since 2026-09-22), comments, spaces, threads, Pulse | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:29-66`). Required: `db` (`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`, fails on 0). Optional: `network_jwks`, `media` (community mode) or `live` (live mode). Reports `pastes_authority`. | **Yes** | Drill passed 03:44 (manual) and 05:22. || **Events** | `/var/lib/openvibe-events/events.db` (`deploy/systemd/openvibe-events.service:30-31`); `EVENTS_DB_PATH`, default `./data/events.db` (`server/config.js:67`). | Yes: the durable event store, subscriptions, deliveries, DLQ | SQLite, the reviewed state. `README.md:163`: "the plan's PostgreSQL + Redis fanout is a later step". No dated plan. | `/api/ready` (`server/app.js:41-66`). Required: `db` (`store.ping()` = `SELECT 1`), `network_jwks`, `delivery_worker` (unless `EVENTS_WORKER=off`). Optional: `dlq` (degraded at more than `EVENTS_DLQ_DEGRADED_AT`, default 100). | **Yes** | Drill passed 05:22. || **Chat** | `/var/lib/openvibe-chat/chat.db` (`deploy/systemd/openvibe-chat.service:38-39`); `CHAT_DB_PATH`, default `./data/chat.db` (`server/config.js:34`). It also reads Live's `/opt/openvibe.live/data/sounds` (`:43`). | Yes, since 2026-09-23 02:03 UTC: the 12 chat tables (`table_authority = 'chat'`). Six staged tables are still Live's. | SQLite, the reviewed state. | `/ready` (loopback). Since `45f2b1f`: required `db` (`SELECT MAX(id) FROM chat_messages`, 503 on failure); optional `live_sync` (degraded when no clean Live sync pass for `LIVE_SYNC_STALE_MS`, 60 s, or one step is late). `mirror.pending` is still reported only. | **Yes** | Drill passed 05:22. |--| **OpenRe.Stream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |++| **OpenRestream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). || **Billing** | `/var/lib/openvibe-billing/billing.db` (`deploy/systemd/openvibe-billing.service:23-25`, `StateDirectoryMode=0700`); `BILLING_DB_PATH`, default `./data/billing.db` (`server/config.js:62`). The journal is append-only (triggers refuse UPDATE and DELETE, `README.md:45-50`). | **Not yet.** It is a shadow: `STATUS.json` `"authoritative": false`. Live holds the money until the cutover. | SQLite, the reviewed state. ADR-007's SERIALIZABLE and row-lock rule applies when money moves to PostgreSQL. No plan yet. | `/api/ready` (`server/app.js:54-60`, own handler). `SELECT 1 FROM settings WHERE id = 1` and Network public key loaded, otherwise 503 `service.not_ready`. `/api/health` reports frozen state and providers. | **Yes** | Drill passed 05:22. || **Tips** | `/var/lib/openvibe-tips/tips.db` (`deploy/systemd/openvibe-tips.service:25`); `TIPS_DB_PATH` (`server/config.js:31`). | Yes: creator tip profiles, interactions, goals | SQLite, the reviewed state. | `/api/ready` (`server/app.js:74-79`, own handler): `SELECT 1 FROM settings WHERE id = 1` and key loaded. | **Yes** | Drill passed 05:23. || **VIP** | `/var/lib/openvibe-vip/vip.db` (`deploy/systemd/openvibe-vip.service:25`); `VIP_DB_PATH` (`server/config.js:31`). | Yes: creators, plans, memberships, perks (checkout closed until the Billing cutover) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:20-48`). Required: `db` (`COUNT(*) FROM vip_creators >= 1`). Optional: `network_jwks`, `billing`. | **Yes** | Drill passed 05:23. |@@@ -152,11 +152,11 @@ reviewed on 2026-09-23.## Deployed-state notes- **Loopback only**, not on their domains yet (operator notes): Tips, VIP, Search, Sources, News, Reviews,-- Codes, AI and OpenRe's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:++ Codes, AI and OpenRestream's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:Billing. Deals, Coupons and Trade passed drills against production, so they run on the host. Whetherthey are public is unknown: check nginx `sites-enabled`. Sites still has placeholders foropenvibe.deals, .coupons and .trade.--- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRe++- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRestreamand AI still say `"deployed": false`, and so do Tips, Search, Sources, News, Reviews, Deals, Coupons,Trade and Codes. `STATUS.json` stays each service's own record to fix. This page does not change it.@@@ -2272,7 +2272,7 @@"table": "migration_map"}],-- "_notes": "Only openre-api starts (command; the working directory and the non-secret Environment= come from openre-api.service). It never runs a transport, a coordinator or the event relay in any mode. OPENRE_DRILL=1 (OpenRe 5e86ea7 and later) also makes it read-only: writes, /play/ (which would pull from production's worker on loopback) and sign-in (which would redeem codes at Network) answer 503. The coordinator and the workers refuse to start. The event relay and Media calls are off. EVENTS_URL and OV_OAUTH_CLIENT_SECRET empty and MEDIA_URL on the closed port are the same switches for releases before 5e86ea7. Left on (read-only): the Network JWKS GET on loopback. Not compared: ingest_sessions and workers (they change whenever someone streams or a generation rolls over) and /api/ready (it reports live worker heartbeats)."++ "_notes": "Only openre-api starts (command; the working directory and the non-secret Environment= come from openre-api.service). It never runs a transport, a coordinator or the event relay in any mode. OPENRE_DRILL=1 (OpenRestream 5e86ea7 and later) also makes it read-only: writes, /play/ (which would pull from production's worker on loopback) and sign-in (which would redeem codes at Network) answer 503. The coordinator and the workers refuse to start. The event relay and Media calls are off. EVENTS_URL and OV_OAUTH_CLIENT_SECRET empty and MEDIA_URL on the closed port are the same switches for releases before 5e86ea7. Left on (read-only): the Network JWKS GET on loopback. Not compared: ingest_sessions and workers (they change whenever someone streams or a generation rolls over) and /api/ready (it reports live worker heartbeats)."},"protected": {"kind": "postgresql-count",@@@ -8,7 +8,7 @@** ovhost deploy|rollback <service> announces after a deploy or rollback that went live* ovhost announce <service> for services deployed by their own scripts (Live, Tools, Sites,-- * OpenRe, Games)++ * OpenRestream, Games)** Best effort: it never fails or holds up a deploy. The token request and the publish each have a* short timeout and the whole announcement a budget of a few seconds; a failure is logged and the@@@ -35,7 +35,7 @@* writes to the checkout, the env file, the database or the backup. It never reads an env value:* systemd loads the production env file for the instance, and the override file holds only* inventory values. Environment= values from the unit are passed on only if their names do not look-- * secret and the drill does not set them itself (OpenRe's unit sets DATABASE_URL to production's++ * secret and the drill does not set them itself (OpenRestream's unit sets DATABASE_URL to production's* database: that line never reaches the drill instance).** The production env file is loaded FIRST, so any variable the drill does not set keeps its@@@ -5,7 +5,7 @@* that break the next pull.*/function git(exec, svc, { dir = svc.repo, safeDirectory = false } = {}) {-- // A release directory may belong to the service user while git runs as the clone's owner (OpenRe++ // A release directory may belong to the service user while git runs as the clone's owner (OpenRestream// chowns each release): -c safe.directory names that one directory, nothing wider.const pre = ['-C', dir, ...(safeDirectory ? ['-c', `safe.directory=${dir}`] : [])];const run = async (args, { allowFail = false } = {}) => {@@@ -52,7 +52,7 @@ function httpsOrigin(value, where) {* untracked lockfiles the release tracks are removed first, preflight loads* static-build an in-place checkout with a build and repo vhosts and no process (Sites)* pnpm-build an in-place pnpm workspace with a build (Games)-- * release-layout releases/<id> behind a `current` symlink (Live, OpenRe): a new release is prepared++ * release-layout releases/<id> behind a `current` symlink (Live, OpenRestream): a new release is prepared* while the old one serves, switching is an atomic rename, rollback switches back*/const STRATEGIES = ['git-checkout', 'multi-app', 'static-build', 'pnpm-build', 'release-layout'];@@@ -210,7 +210,7 @@ function normaliseService(id, raw, defaults) {announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },units: s.units || [],// Units that belong to the service but that ovhost never starts, stops or restarts: e.g.-- // OpenRe's transport workers ([email protected], one instance per release), which++ // OpenRestream's transport workers ([email protected], one instance per release), which// drain on their own. status/validate list their instances read-only.workerUnits: s.workerUnits || [],socketUnit: s.socketUnit || null,@@@ -9,7 +9,7 @@* Media: SELECT count(*) FROM vods WHERE is_recording = 1.* postgresql-count a read-only SELECT against a database in the cluster, run as the postgres OS* user over peer auth (no secret is read, and a PostgreSQL service's database has-- * no file for a sqlite-count to open). OpenRe: ingest sessions in flight.++ * no file for a sqlite-count to open). OpenRestream: ingest sessions in flight.* sum the total of `probes` (each an http-json-count, sqlite-count or* postgresql-count): Tools' running* jobs in each satellite's jobs.db. Unknown when any part is.@@@ -1,11 +1,11 @@'use strict';/*** The release-layout strategy (roadmap WS-N task 11): Live's deploy/scripts/deploy.sh (release layout)-- * and OpenRe.Stream's deploy.sh `release` + `api`, as one engine.++ * and OpenRestream's deploy.sh `release` + `api`, as one engine.** <repo>/repo the git clone releases are made from (release.git), owned by `owner`* <repo>/releases/<id> a detached worktree per release with its own node_modules-- * (id: <UTC yyyymmdd-HHMMSS>-<sha8> for Live, <sha12> for OpenRe)++ * (id: <UTC yyyymmdd-HHMMSS>-<sha8> for Live, <sha12> for OpenRestream)* <repo>/current -> releases/<id>, switched by an atomic rename (ln -s + mv -T)* release.links links made in every release (Live: data -> ../../shared/data)*@@@ -13,7 +13,7 @@* 1. fetch; the target is origin/<branch> (or --to); nothing new and no --restart: done.* 2. prepare the new release while the current one serves: worktree, links, `npm ci` when the* lockfile or dependency fields changed (else the current node_modules hard-linked: cp -al), every-- * dependency resolves, preflight (Live: syntax check of the changed files), release.chown (OpenRe).++ * dependency resolves, preflight (Live: syntax check of the changed files), release.chown (OpenRestream).* Any failure removes the new release: the current one is untouched and nothing restarted (exit 2).* 3. static-only change (everything outside noRestartPaths unchanged, unit files unchanged): switch* `current`, wait release.settleSeconds, the ready URL must answer, else switch back (exit 3).@@@ -23,7 +23,7 @@* new sha when ready.release). Not ready: switch back, put the previous release's unit files back,* restart, exit 3; if that is not ready either, exit 4 (MANUAL INTERVENTION).* 5. prune to release.keep, never the current release, the one just left, or one a worker unit-- * instance (OpenRe's openre-rtmp-ingest@<id>) still runs from.++ * instance (OpenRestream's openre-rtmp-ingest@<id>) still runs from.** The socket rule (Live): pid 1 must hold the socket unit's listener. When the socket unit file changed* in this deploy, or systemd does not hold the listener, the service is stopped, the socket restarted@@@ -31,7 +31,7 @@* only the service restarts and new connections queue on the socket. A listener still not systemd's* after readiness is reported loudly and recorded (socketHeld: false).*-- * --prepare-only (OpenRe's `deploy.sh release`): step 2 and stop; a later `deploy --to <sha>` of the++ * --prepare-only (OpenRestream's `deploy.sh release`): step 2 and stop; a later `deploy --to <sha>` of the* same commit switches to it (sha12 ids name one release per commit, so it is reused).** rollback: back to the release the release log says the current one replaced (or --to <id|sha>, or@@@ -1,7 +1,7 @@'use strict';/*** plan, deploy and rollback. A service's `strategy` (lib/inventory.js, docs/deploy-strategies.md)-- * picks the engine: release-layout services (Live, OpenRe) go to lib/release-layout.js; every other++ * picks the engine: release-layout services (Live, OpenRestream) go to lib/release-layout.js; every other* strategy (git-checkout, multi-app, static-build, pnpm-build) is an in-place git checkout driven* here, with the strategy's defaults. The rules this encodes come from the per-repository deploy* scripts it replaces:@@@ -13,7 +13,7 @@# deploys (ovhost deploy), and makes the SQLite file read-only: it is the 7-day rollback (compatibility# register C-89: sudo ovhost rollback <svc> to the release before, which reads the file).#--# A service that is not one unit in /opt/openvibe.<svc> (OpenRe: two units plus per-release transport workers, a++# A service that is not one unit in /opt/openvibe.<svc> (OpenRestream: two units plus per-release transport workers, a# release layout) sets, in the environment:# SWITCH_UNITS="a.service b.service 'worker@*.service'" every unit to stop (the first one's environment runs the# import); a pattern stops every running instance@@@ -22,7 +22,7 @@ const KEY_HEX = crypto.createHash('sha256').update('test backup key').digest('heconst S3_SECRET = `${SECRET}-s3`;const TENANT_BLOB = 'tenant object bytes\n';--/** The standard fake host plus AI (two databases) and OpenRe (not deployed), a backup env and key. */++/** The standard fake host plus AI (two databases) and OpenRestream (not deployed), a backup env and key. */function backupHost({ envMode = 0o600, keyMode = 0o600, envOwner = 'root', env = null } = {}) {const host = scenario();const doc = JSON.parse(host.read('/etc/openvibe/host.json'));@@@ -26,7 +26,7 @@ runTests([// Every service on the host is deployed by a strategy (WS-N task 11): Games by pnpm-build.assert.ok(Object.values(inv.services).every((s) => s.managed), 'every service is managed');assert.deepStrictEqual(Object.fromEntries(['live', 'tools', 'sites', 'games', 'openre', 'network'].map((id) => [id, inv.services[id].strategy])), { live: 'release-layout', tools: 'multi-app', sites: 'static-build', games: 'pnpm-build', openre: 'release-layout', network: 'git-checkout' });-- // Live's and OpenRe's release layouts are root-owned clones (git as root); their services and drills run as ubuntu.++ // Live's and OpenRestream's release layouts are root-owned clones (git as root); their services and drills run as ubuntu.for (const s of Object.values(inv.services)) assert.strictEqual(s.owner, ['live', 'openre'].includes(s.id) ? 'root' : 'ubuntu', `${s.id} checkout owner`);assert.strictEqual(inv.services.live.runAs, 'ubuntu');assert.strictEqual(inv.services.openre.runAs, 'ubuntu');@@@ -1,6 +1,6 @@'use strict';/**-- * OpenRe on ovhost: `workerUnits` (the transport worker templates) are listed by status and++ * OpenRestream on ovhost: `workerUnits` (the transport worker templates) are listed by status and* validate and never started, stopped or restarted; the inventory refuses a worker unit in* `units`; a deploy of a git-layout service with worker units restarts only its units; and the* example inventory's openre drill starts openre-api alone with OPENRE_DRILL=1 and the side-effect@@@ -22,7 +22,7 @@ const WORKERS = [`openre-webrtc@${RELEASE}.service`,];--/** The standard fake host plus OpenRe as on openvibe-ovh: release layout, two units, worker instances. */++/** The standard fake host plus OpenRestream as on openvibe-ovh: release layout, two units, worker instances. */async function openreHost({ workersRunning = true } = {}) {const host = scenario();const doc = JSON.parse(host.read('/etc/openvibe/host.json'));@@@ -91,7 +91,7 @@ async function openreHost({ workersRunning = true } = {}) {const stateChanges = (host) => host.calls.filter((c) => c.cmd === 'systemctl' && !['show', 'is-active', 'is-enabled', 'cat', 'status', 'list-units'].includes(c.args[0]));runTests([-- test('the example inventory declares the OpenRe worker templates and a supported drill', () => {++ test('the example inventory declares the OpenRestream worker templates and a supported drill', () => {const inv = normalise(EXAMPLE);const o = inv.services.openre;assert.deepStrictEqual(o.units, ['openre-api.service', 'openre-session-coordinator.service']);@@@ -151,7 +151,7 @@ async function liveHost(overrides = {}) {return host;}--// ── OpenRe: release layout, sha12 ids, workers ───────────────────────────────++// ── OpenRestream: release layout, sha12 ids, workers ───────────────────────────────function openreEntry(overrides = {}) {return {@@@ -1,7 +1,7 @@'use strict';/**-- * The release-layout strategy (lib/release-layout.js) against the fake host, as Live and OpenRe run on-- * the real one: every rule of Live's deploy/scripts/deploy.sh (release layout) and OpenRe's++ * The release-layout strategy (lib/release-layout.js) against the fake host, as Live and OpenRestream run on++ * the real one: every rule of Live's deploy/scripts/deploy.sh (release layout) and OpenRestream's* deploy.sh release + api, driven through `ovhost plan|deploy|rollback`.*/const assert = require('assert');@@@ -318,7 +318,7 @@ runTests([assert.strictEqual(legacy.services.x.managed, false, 'a release layout without the strategy stays with its own script');}),-- // ── OpenRe ──++ // ── OpenRestream ──test('openre: release <sha12> with npm ci and chown to ubuntu, API and coordinator restarted, workers never touched', async () => {const host = await openreHost();const first = host.firstRelease;@@@ -327,7 +327,7 @@ runTests([assert.strictEqual(r.code, 0, r.out);assert.strictEqual(host.current(), to.slice(0, 12));const npm = host.calls.filter((c) => c.cmd === 'npm');-- assert.strictEqual(npm.length, 1, 'OpenRe installs into every release (reuseModules false)');++ assert.strictEqual(npm.length, 1, 'OpenRestream installs into every release (reuseModules false)');assert.strictEqual(npm[0].cwd, `/opt/openre.stream/releases/${to.slice(0, 12)}`);assert.ok(host.calls.some((c) => c.cmd === 'chown' && c.args.join(' ') === `-R ubuntu:ubuntu /opt/openre.stream/releases/${to.slice(0, 12)}`));assert.strictEqual(host.files.get(`/opt/openre.stream/releases/${to.slice(0, 12)}/server/index.js`).owner, 'ubuntu');
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> The network's deployment/control plane first; then isolated hosting for community sites, bots and mods.
> The network's deployment/control plane first; then isolated hosting for community sites, bots and mods.
-**Status:** alpha. Stage A (operator plane) is a CLI, `ovhost`, tested against a fake host. It is installed on the production host (2026-09-23, `/usr/local/bin/ovhost`, inventory `/etc/openvibe/host.json`) and it deploys 20 services (`ovhost deploy <svc>`: Network, Events, Chat, Billing, Tips, VIP, AI, Search, Sources, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes, Host, Media, Community; 70+ releases since 2026-09-23, with automatic rollback on a failed readiness check) and runs the nightly backups and the restore drills of 19 services. Live, Tools, Sites, Games and OpenRe moved to it in the 2026-09-27 cutover (roadmap WS-N task 11) through the deploy strategies `release-layout`, `multi-app`, `static-build` and `pnpm-build`, so every service now deploys with `sudo ovhost deploy <svc>` (Live with `--wait-idle`); each of those repositories keeps `deploy/scripts/deploy.sh` as a thin wrapper that hands over to ovhost or falls back to its old script: [docs/deploy-strategies.md](docs/deploy-strategies.md). Stage B (tenant static hosting) is a service, written and tested, and **launched on 2026-10-07**: `openvibe.host` serves the dashboard, API and front page, and `*.openvibe.host` serves tenant sites (vhosts from `ovhost nginx tenants host --install`; the Sites placeholder and the pending 404 vhost are gone; certificate lifecycle on `openvibe-certs.timer`; an nginx catch-all refuses hosts no site names). The launch record, verification and rollback: [docs/launch.md](docs/launch.md); the threat review: [docs/threat-review.md](docs/threat-review.md). Stage C (sandboxed user code) is **not started**.
+**Status:** alpha. Stage A (operator plane) is a CLI, `ovhost`, tested against a fake host. It is installed on the production host (2026-09-23, `/usr/local/bin/ovhost`, inventory `/etc/openvibe/host.json`) and it deploys 20 services (`ovhost deploy <svc>`: Network, Events, Chat, Billing, Tips, VIP, AI, Search, Sources, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes, Host, Media, Community; 70+ releases since 2026-09-23, with automatic rollback on a failed readiness check) and runs the nightly backups and the restore drills of 19 services. Live, Tools, Sites, Games and OpenRestream moved to it in the 2026-09-27 cutover (roadmap WS-N task 11) through the deploy strategies `release-layout`, `multi-app`, `static-build` and `pnpm-build`, so every service now deploys with `sudo ovhost deploy <svc>` (Live with `--wait-idle`); each of those repositories keeps `deploy/scripts/deploy.sh` as a thin wrapper that hands over to ovhost or falls back to its old script: [docs/deploy-strategies.md](docs/deploy-strategies.md). Stage B (tenant static hosting) is a service, written and tested, and **launched on 2026-10-07**: `openvibe.host` serves the dashboard, API and front page, and `*.openvibe.host` serves tenant sites (vhosts from `ovhost nginx tenants host --install`; the Sites placeholder and the pending 404 vhost are gone; certificate lifecycle on `openvibe-certs.timer`; an nginx catch-all refuses hosts no site names). The launch record, verification and rollback: [docs/launch.md](docs/launch.md); the threat review: [docs/threat-review.md](docs/threat-review.md). Stage C (sandboxed user code) is **not started**.
**Domain:** `openvibe.host` (dashboard and API) and `*.openvibe.host` (tenant sites). Launched on 2026-10-07 (see [Launch rule](#launch-rule)); OpenVibe.Sites no longer serves it.
**Domain:** `openvibe.host` (dashboard and API) and `*.openvibe.host` (tenant sites). Launched on 2026-10-07 (see [Launch rule](#launch-rule)); OpenVibe.Sites no longer serves it.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.3 and §15.17; roadmap Wave 21.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.3 and §15.17; roadmap Wave 21.
**License:** AGPL-3.0 (same as every OpenVibe service).
**License:** AGPL-3.0 (same as every OpenVibe service).
@@ -20,7 +20,7 @@ Every repository ships its own deploy script, and each one encodes rules learned
@@ -20,7 +20,7 @@ Every repository ships its own deploy script, and each one encodes rules learned
| Install only when the lockfile or dependencies changed; restore `package-lock.json` afterwards | Network `deploy.sh` and host practice | `npm install --omit=dev` as the checkout owner, then `git checkout -- package-lock.json` if npm rewrote it. |
| Install only when the lockfile or dependencies changed; restore `package-lock.json` afterwards | Network `deploy.sh` and host practice | `npm install --omit=dev` as the checkout owner, then `git checkout -- package-lock.json` if npm rewrote it. |
| Every dependency must resolve before anything restarts | Tools `deploy.sh` (an emptied `file:` link crash-looped every unit) | `node_modules/<dep>/package.json` must exist, parse and name the package. A broken dependency is reinstalled once, and if it still fails the deploy aborts and the checkout is restored. |
| Every dependency must resolve before anything restarts | Tools `deploy.sh` (an emptied `file:` link crash-looped every unit) | `node_modules/<dep>/package.json` must exist, parse and name the package. A broken dependency is reinstalled once, and if it still fails the deploy aborts and the checkout is restored. |
| Build, install vhosts, `nginx -t`, reload | Sites `deploy.sh` | `build` hooks and `nginx.installOnDeploy`. Vhost installs are transactional: if `nginx -t` fails, the previous files come back and nginx is not reloaded. |
| Build, install vhosts, `nginx -t`, reload | Sites `deploy.sh` | `build` hooks and `nginx.installOnDeploy`. Vhost installs are transactional: if `nginx -t` fails, the previous files come back and nginx is not reloaded. |
-| Build a release while the old one serves; switch a symlink; roll back to a release with its own `node_modules`; pid 1 must hold the socket | Live `deploy.sh` (release layout), OpenRe `deploy.sh release`/`api` | Strategy `release-layout` ([docs/deploy-strategies.md](docs/deploy-strategies.md)). The socket unit is restarted only when its unit file changed or systemd does not hold the port. |
+| Build a release while the old one serves; switch a symlink; roll back to a release with its own `node_modules`; pid 1 must hold the socket | Live `deploy.sh` (release layout), OpenRestream `deploy.sh release`/`api` | Strategy `release-layout` ([docs/deploy-strategies.md](docs/deploy-strategies.md)). The socket unit is restarted only when its unit file changed or systemd does not hold the port. |
| Per-app installs (not `apps/_shared`), jobs runtime and guard load before the restart, every `openvibe-tools*` unit | Tools `deploy.sh` | Strategy `multi-app`: `skipPackages`, `preflight`, `unitsMatch`, `ready.release`. |
| Per-app installs (not `apps/_shared`), jobs runtime and guard load before the restart, every `openvibe-tools*` unit | Tools `deploy.sh` | Strategy `multi-app`: `skipPackages`, `preflight`, `unitsMatch`, `ready.release`. |
| git as the checkout owner; never leave root-owned files under `/opt` | Community/Events/Tools checkouts | git, npm, builds, SQLite queries and backups all run as the declared owner/service user (`runuser` from root, `sudo -u` otherwise); PostgreSQL dumps and probes run as the postgres OS user. |
| git as the checkout owner; never leave root-owned files under `/opt` | Community/Events/Tools checkouts | git, npm, builds, SQLite queries and backups all run as the declared owner/service user (`runuser` from root, `sudo -u` otherwise); PostgreSQL dumps and probes run as the postgres OS user. |
@@ -150,7 +150,7 @@ Every command accepts `--json` and `--inventory <file>`. `drill` exits `0` passe
@@ -150,7 +150,7 @@ Every command accepts `--json` and `--inventory <file>`. `drill` exits `0` passe
`service add` runs as root against `/etc/openvibe/host.json`. It copies `services.<id>` from the installed CLI's `host.example.json`, drops `_note`, and backs up the live inventory before changing it. The example entry may set `cloneUrl` to a credential-free GitHub HTTPS URL; otherwise the repository is `https://github.com/OpenVibers/OpenVibe.<Capitalized id>.git`. The command refuses an existing entry, occupied checkout or env file. It holds the inventory lock (`<stateDir>/locks/_inventory.lock`) from reading the inventory to the end, and refuses a symlinked `.env.example`, which it reads with the checkout owner's permissions. It clones as the checkout owner, creates the entry's `envFile` (`/etc/openvibe/<id>.env`, 0600) as a copy of the checkout's `.env.example` with production `NODE_ENV`, `HOST` and `PORT`. It sets `BASE_URL` when the inventory vhost, origin or env example supplies a public HTTPS origin; otherwise it leaves an existing empty `BASE_URL` for the operator and does not add one to services without it. An explicitly required `BASE_URL` needs an inventory vhost or origin before setup begins. The tracked `.env.example` is never changed. It runs `data provision` if that example declares `DATABASE_URL`. Output lists env names but never values. `--dry-run` shows the plan without changing files. It then lists the env names still empty (leaving out the production overrides and, after provisioning, `DATABASE_URL`, `DATABASE_DIRECT_URL`, `VALKEY_URL`, `VALKEY_PREFIX`). When the example declares `OV_OAUTH_CLIENT_ID` or `OV_OAUTH_CLIENT_SECRET`, it prints one root command for the Network service principal. The command runs `server/setup/service-principal.js` through `systemd-run` with Network's env file (`services.network.envFile`, default `/etc/openvibe/network.env`) in Network's checkout (`services.network.repo`, default `/opt/openvibe.network`). It uses `rotate` when `list` already shows the id and `create` otherwise, so the principal never lands in a development database (the secret is never printed). It ends with `then: ovhost validate <id>; ovhost deploy <id> --restart`; ovhost runs neither. If a later step fails, the inventory backup path is printed; completed setup steps are not automatically undone.
`service add` runs as root against `/etc/openvibe/host.json`. It copies `services.<id>` from the installed CLI's `host.example.json`, drops `_note`, and backs up the live inventory before changing it. The example entry may set `cloneUrl` to a credential-free GitHub HTTPS URL; otherwise the repository is `https://github.com/OpenVibers/OpenVibe.<Capitalized id>.git`. The command refuses an existing entry, occupied checkout or env file. It holds the inventory lock (`<stateDir>/locks/_inventory.lock`) from reading the inventory to the end, and refuses a symlinked `.env.example`, which it reads with the checkout owner's permissions. It clones as the checkout owner, creates the entry's `envFile` (`/etc/openvibe/<id>.env`, 0600) as a copy of the checkout's `.env.example` with production `NODE_ENV`, `HOST` and `PORT`. It sets `BASE_URL` when the inventory vhost, origin or env example supplies a public HTTPS origin; otherwise it leaves an existing empty `BASE_URL` for the operator and does not add one to services without it. An explicitly required `BASE_URL` needs an inventory vhost or origin before setup begins. The tracked `.env.example` is never changed. It runs `data provision` if that example declares `DATABASE_URL`. Output lists env names but never values. `--dry-run` shows the plan without changing files. It then lists the env names still empty (leaving out the production overrides and, after provisioning, `DATABASE_URL`, `DATABASE_DIRECT_URL`, `VALKEY_URL`, `VALKEY_PREFIX`). When the example declares `OV_OAUTH_CLIENT_ID` or `OV_OAUTH_CLIENT_SECRET`, it prints one root command for the Network service principal. The command runs `server/setup/service-principal.js` through `systemd-run` with Network's env file (`services.network.envFile`, default `/etc/openvibe/network.env`) in Network's checkout (`services.network.repo`, default `/opt/openvibe.network`). It uses `rotate` when `list` already shows the id and `create` otherwise, so the principal never lands in a development database (the secret is never printed). It ends with `then: ovhost validate <id>; ovhost deploy <id> --restart`; ovhost runs neither. If a later step fails, the inventory backup path is printed; completed setup steps are not automatically undone.
-**Deploy strategies** (roadmap WS-N task 11). The inventory entry's `strategy` picks how a service deploys: `git-checkout` (the default: an in-place checkout, every service above), `multi-app` (Tools), `static-build` (Sites), `pnpm-build` (Games) or `release-layout` (Live, OpenRe: `releases/<id>` behind a `current` symlink, prepared while the old release serves, `--prepare-only` to stop after preparing). Each strategy carries the rules of the script it replaces: preflight checks, build output restored from git, untracked lockfiles, `unitsMatch`, `/release.json` naming the new sha, the socket rule. [docs/deploy-strategies.md](docs/deploy-strategies.md) has the engines, every inventory field, the rule-by-rule mapping from each script, what was not ported and why, the production cutover checklist and the proposed inventory diff.
+**Deploy strategies** (roadmap WS-N task 11). The inventory entry's `strategy` picks how a service deploys: `git-checkout` (the default: an in-place checkout, every service above), `multi-app` (Tools), `static-build` (Sites), `pnpm-build` (Games) or `release-layout` (Live, OpenRestream: `releases/<id>` behind a `current` symlink, prepared while the old release serves, `--prepare-only` to stop after preparing). Each strategy carries the rules of the script it replaces: preflight checks, build output restored from git, untracked lockfiles, `unitsMatch`, `/release.json` naming the new sha, the socket rule. [docs/deploy-strategies.md](docs/deploy-strategies.md) has the engines, every inventory field, the rule-by-rule mapping from each script, what was not ported and why, the production cutover checklist and the proposed inventory diff.
`deploy --browser-check` checks the service's public site in headless Chrome after a deploy that went through. It runs [`scripts/browser-check.js`](docs/browser-check.md) for that one site. The check is report only: it never changes the exit code or the release record. It needs Chrome where ovhost runs.
`deploy --browser-check` checks the service's public site in headless Chrome after a deploy that went through. It runs [`scripts/browser-check.js`](docs/browser-check.md) for that one site. The check is report only: it never changes the exit code or the release record. It needs Chrome where ovhost runs.
@@ -210,7 +210,7 @@ The sandbox blocks writes outside the drill directory and addresses beyond loopb
@@ -210,7 +210,7 @@ The sandbox blocks writes outside the drill directory and addresses beyond loopb
### Inventory
### Inventory
-`host.example.json` describes every service on the host. It records the checkout, owner/`runAs`, units and socket unit, `unitSources` (repo unit files), env file, `env.required` (names, or `"from-example"`), port, loopback `ready` URL (plus headers, since Tools needs `Host: openvibe.tools`), `packages` (`"apps/*"` for Tools), install command, `build` hooks, `noRestartPaths`, the `protected` probe (`http-json-count`, `sqlite-count`, `postgresql-count`, or `sum` of several: Live streams, Media recordings, Events SSE connections, Chat sockets, Games players online, OpenRe ingest sessions, Tools running jobs), the `drain` policy, `databases` (each a SQLite file `{ "name", "path" }` or a PostgreSQL database `{ "name", "engine": "postgresql", "database": "ov_<name>" }`; [docs/db-inventory.md](docs/db-inventory.md)), `backupOnChange`, `nginx`, `layout` (`release` for a `releases/<id>` + `current` service deployed by its own script, which ovhost does not manage) and `workerUnits`. `workerUnits` are units, or templates such as OpenRe's `[email protected]`, that belong to the service but that ovhost **never starts, stops or restarts**: they drain and exit on their own. `status` and `validate` list their instances read-only, `validate` warns when none runs, and the inventory refuses a worker unit, or an instance of one, in `units`. The real file is `/etc/openvibe/host.json` and is **never committed**. When `ovhost` runs as root it refuses an inventory that is not root-owned or that anyone else can write, and it rejects relative paths, non-loopback probe URLs, non-SELECT probe queries and vhost names that contain a path.
+`host.example.json` describes every service on the host. It records the checkout, owner/`runAs`, units and socket unit, `unitSources` (repo unit files), env file, `env.required` (names, or `"from-example"`), port, loopback `ready` URL (plus headers, since Tools needs `Host: openvibe.tools`), `packages` (`"apps/*"` for Tools), install command, `build` hooks, `noRestartPaths`, the `protected` probe (`http-json-count`, `sqlite-count`, `postgresql-count`, or `sum` of several: Live streams, Media recordings, Events SSE connections, Chat sockets, Games players online, OpenRestream ingest sessions, Tools running jobs), the `drain` policy, `databases` (each a SQLite file `{ "name", "path" }` or a PostgreSQL database `{ "name", "engine": "postgresql", "database": "ov_<name>" }`; [docs/db-inventory.md](docs/db-inventory.md)), `backupOnChange`, `nginx`, `layout` (`release` for a `releases/<id>` + `current` service deployed by its own script, which ovhost does not manage) and `workerUnits`. `workerUnits` are units, or templates such as OpenRestream's `[email protected]`, that belong to the service but that ovhost **never starts, stops or restarts**: they drain and exit on their own. `status` and `validate` list their instances read-only, `validate` warns when none runs, and the inventory refuses a worker unit, or an instance of one, in `units`. The real file is `/etc/openvibe/host.json` and is **never committed**. When `ovhost` runs as root it refuses an inventory that is not root-owned or that anyone else can write, and it rejects relative paths, non-loopback probe URLs, non-SELECT probe queries and vhost names that contain a path.
**Lifecycle** (roadmap WS-P task 1, `lib/lifecycle.js`). Each service's lifecycle is the `lifecycle` block of its manifest in openvibe-contracts (≥ 0.55.0): liveness, shutdown (signal, `deadlineSeconds`, drains, workers), startupRecovery, rollback, contracts and leases. A service entry may carry its own `lifecycle` block, which replaces the manifest's on this host, and `validate --manifest <file>` reads another manifest file. `ovhost validate` fails when no block is found or a field is missing, naming the service and the field (`live: lifecycle.shutdown.deadlineSeconds is missing`). It also fails when `deadlineSeconds` exceeds a unit's stop timeout (systemd's effective `TimeoutStopUSec`, else `TimeoutStopSec` in the unit source) or the unit's `KillSignal` is another signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`. Worker units that drain longer than their stop timeout are a warning, since ovhost never stops them. This repository still pins openvibe-contracts v0.49.0, which has no lifecycle blocks, so until that pin moves to v0.55.0 or later `validate` reports every service as undeclared.
**Lifecycle** (roadmap WS-P task 1, `lib/lifecycle.js`). Each service's lifecycle is the `lifecycle` block of its manifest in openvibe-contracts (≥ 0.55.0): liveness, shutdown (signal, `deadlineSeconds`, drains, workers), startupRecovery, rollback, contracts and leases. A service entry may carry its own `lifecycle` block, which replaces the manifest's on this host, and `validate --manifest <file>` reads another manifest file. `ovhost validate` fails when no block is found or a field is missing, naming the service and the field (`live: lifecycle.shutdown.deadlineSeconds is missing`). It also fails when `deadlineSeconds` exceeds a unit's stop timeout (systemd's effective `TimeoutStopUSec`, else `TimeoutStopSec` in the unit source) or the unit's `KillSignal` is another signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`. Worker units that drain longer than their stop timeout are a warning, since ovhost never stops them. This repository still pins openvibe-contracts v0.49.0, which has no lifecycle blocks, so until that pin moves to v0.55.0 or later `validate` reports every service as undeclared.
@@ -39,7 +39,7 @@
@@ -39,7 +39,7 @@
"host.release.published"
"host.release.published"
],
],
"stages": {
"stages": {
- "A": "alpha: ovhost CLI installed on the production host 2026-09-23 (/usr/local/bin/ovhost, inventory /etc/openvibe/host.json); every service deploys with `sudo ovhost deploy <svc>` since the 2026-09-27 cutover (roadmap WS-N task 11: Live, Tools, Sites, Games and OpenRe through the release-layout, multi-app, static-build and pnpm-build strategies); nightly backups, restore drills (19 passed 2026-09-23, docs/restore-drills.md), release notifications, incidents and maintenance, the alerts relay; the D41 deploy-and-rollback proof (scripts/d41-proof.sh) is written and tested",
+ "A": "alpha: ovhost CLI installed on the production host 2026-09-23 (/usr/local/bin/ovhost, inventory /etc/openvibe/host.json); every service deploys with `sudo ovhost deploy <svc>` since the 2026-09-27 cutover (roadmap WS-N task 11: Live, Tools, Sites, Games and OpenRestream through the release-layout, multi-app, static-build and pnpm-build strategies); nightly backups, restore drills (19 passed 2026-09-23, docs/restore-drills.md), release notifications, incidents and maintenance, the alerts relay; the D41 deploy-and-rollback proof (scripts/d41-proof.sh) is written and tested",
"B": "launched 2026-10-07 at openvibe.host (dashboard, API, front page) and *.openvibe.host (tenant sites); docs/launch.md, threat review in docs/threat-review.md",
"B": "launched 2026-10-07 at openvibe.host (dashboard, API, front page) and *.openvibe.host (tenant sites); docs/launch.md, threat review in docs/threat-review.md",
"C": "not started (deliberately: blocked until isolation and metering are proven)"
"C": "not started (deliberately: blocked until isolation and metering are proven)"
},
},
@@ -28,7 +28,7 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat
@@ -28,7 +28,7 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat
| Status on 2026-09-23 | Entries |
| Status on 2026-09-23 | Entries |
|---|---|
|---|---|
| Cut over; the shim is now only a rollback lever | C-01–C-03 (chat), C-10–C-13 (pastes), C-20–C-21 (AI) |
| Cut over; the shim is now only a rollback lever | C-01–C-03 (chat), C-10–C-13 (pastes), C-20–C-21 (AI) |
-| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRe) |
+| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRestream) |
| Running in parallel, waiting on telemetry | C-60–C-61 (Events v1 signatures) |
| Running in parallel, waiting on telemetry | C-60–C-61 (Events v1 signatures) |
| Legacy secret or credential still accepted | C-42, C-70, C-71 (Media app keys) |
| Legacy secret or credential still accepted | C-42, C-70, C-71 (Media app keys) |
| Legacy data kept for rollback | C-72 (analytics backups with IPs), C-73 (frozen Live tables) |
| Legacy data kept for rollback | C-72 (analytics backups with IPs), C-73 (frozen Live tables) |
@@ -75,13 +75,13 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat
@@ -75,13 +75,13 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat
| C-31 | **`money_writes_frozen`** (site setting, owner-only, both modes), with `/api/admin/money/freeze`. | Live `server/monetization/money-authority.js:54-111`, `server/admin/routes.js:492-517` | Live | A permanent incident control, not a shim. It stays. | kept | It is itself the lever. |
| C-31 | **`money_writes_frozen`** (site setting, owner-only, both modes), with `/api/admin/money/freeze`. | Live `server/monetization/money-authority.js:54-111`, `server/admin/routes.js:492-517` | Live | A permanent incident control, not a shim. It stays. | kept | It is itself the lever. |
| C-32 | **Live's provider webhooks** `/api/powerchat/webhook` and `/api/payments/webhook/*`. They answer 410 under `billing`. | Live `server/monetization/payments-routes.js:15-25` | Live | *proposed*: removed with C-30's write paths. A 410 makes PowerChat disable the endpoint (Billing `docs/live-cutover.md:170-175`), so remove them only after PowerChat points at Billing. | cutover + 30 days | Re-point the webhook back to Live. |
| C-32 | **Live's provider webhooks** `/api/powerchat/webhook` and `/api/payments/webhook/*`. They answer 410 under `billing`. | Live `server/monetization/payments-routes.js:15-25` | Live | *proposed*: removed with C-30's write paths. A 410 makes PowerChat disable the endpoint (Billing `docs/live-cutover.md:170-175`), so remove them only after PowerChat points at Billing. | cutover + 30 days | Re-point the webhook back to Live. |
-## Stream ingest (OpenRe deployed; no slot switched)
+## Stream ingest (OpenRestream deployed; no slot switched)
| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
|---|---|---|---|---|---|---|
|---|---|---|---|---|---|---|
-| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRe | *documented*: "Live's ingest code stays until the last protocol has run on OpenRe for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRe `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRe `README.md:205`). |
-| C-41 | **OpenRe port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRe `server/config.js:24,72`, `README.md:139-145` | OpenRe | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |
-| C-42 | **OpenRe `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRe `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRe + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |
+| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRestream | *documented*: "Live's ingest code stays until the last protocol has run on OpenRestream for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRestream `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRestream `README.md:205`). |
+| C-41 | **OpenRestream port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRestream `server/config.js:24,72`, `README.md:139-145` | OpenRestream | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |
+| C-42 | **OpenRestream `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRestream `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRestream + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |
## The shared internal key (`INTERNAL_API_KEY` / `X-Internal-Key`): retired 2026-09-29
## The shared internal key (`INTERNAL_API_KEY` / `X-Internal-Key`): retired 2026-09-29
@@ -157,7 +157,7 @@ These match the search terms but are permanent. They are listed so the next revi
@@ -157,7 +157,7 @@ These match the search terms but are permanent. They are listed so the next revi
- **Network `identity_legacy_map`** and `POST /internal/identity/legacy-map`: ADR-001 keeps legacy ids alongside subject ids permanently (Network `server/identity/subjects.js:25-35`). The *key* on that route is C-50.
- **Network `identity_legacy_map`** and `POST /internal/identity/legacy-map`: ADR-001 keeps legacy ids alongside subject ids permanently (Network `server/identity/subjects.js:25-35`). The *key* on that route is C-50.
- **Network legacy public-key shape**: `public_key` PEM is served beside the spec shape (`server/index.js:371-386`). Billing, Blog, Chat, Codes, Community, Coupons, AI, Host, Media and Events read `public_key`. Removing it would be a breaking change for all of them.
- **Network legacy public-key shape**: `public_key` PEM is served beside the spec shape (`server/index.js:371-386`). Billing, Blog, Chat, Codes, Community, Coupons, AI, Host, Media and Events read `public_key`. Removing it would be a breaking change for all of them.
- **Network `my.openvibe.network`**: 301 to the apex (`server/index.js:571`).
- **Network `my.openvibe.network`**: 301 to the apex (`server/index.js:571`).
-- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRe use their own handlers.
+- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRestream use their own handlers.
- **Analytics `ip`/`user_id`/`city` columns** kept and always NULL: Network `server/analytics/schema.js:1-14`, Tools `apps/_shared/analytics/schema.js:3-6`.
- **Analytics `ip`/`user_id`/`city` columns** kept and always NULL: Network `server/analytics/schema.js:1-14`, Tools `apps/_shared/analytics/schema.js:3-6`.
- **Billing `legacy_order_id`/`legacy_live_txn`/`legacy_live_id`**: import provenance (`server/db.js:120,142,162`). Tips `legacy_source` and VIP `vip_migration_maps` are the same kind of provenance.
- **Billing `legacy_order_id`/`legacy_live_txn`/`legacy_live_id`**: import provenance (`server/db.js:120,142,162`). Tips `legacy_source` and VIP `vip_migration_maps` are the same kind of provenance.
- **Tools registry fallback list** until Network answers (`registry/services.js:16-24`), and `isLegacyHost` for the old paste/login hosts (`index.js:104-105`).
- **Tools registry fallback list** until Network answers (`registry/services.js:16-24`), and `isLegacyHost` for the old paste/login hosts (`index.js:104-105`).
@@ -29,13 +29,13 @@ State markers:
@@ -29,13 +29,13 @@ State markers:
| C | AI → OpenVibe.AI (Wave 13) | **DONE** 2026-09-23 ~15:40 UTC (`AI_SERVICE=remote`) | AI [`docs/migration.md`](https://github.com/OpenVibers/OpenVibe.AI/blob/main/docs/migration.md) | Unset `AI_SERVICE`, restart Live |
| C | AI → OpenVibe.AI (Wave 13) | **DONE** 2026-09-23 ~15:40 UTC (`AI_SERVICE=remote`) | AI [`docs/migration.md`](https://github.com/OpenVibers/OpenVibe.AI/blob/main/docs/migration.md) | Unset `AI_SERVICE`, restart Live |
| D | Events delivery signature v2 | **IN PROGRESS**: Events sends v2; consumer deploys unknown; v1 still sent | Events [`docs/replay-window-rollout.md`](https://github.com/OpenVibers/OpenVibe.Events/blob/main/docs/replay-window-rollout.md) | The consumer redeploys its previous release; replay the DLQ |
| D | Events delivery signature v2 | **IN PROGRESS**: Events sends v2; consumer deploys unknown; v1 still sent | Events [`docs/replay-window-rollout.md`](https://github.com/OpenVibers/OpenVibe.Events/blob/main/docs/replay-window-rollout.md) | The consumer redeploys its previous release; replay the DLQ |
| E | Money → OpenVibe.Billing | **BLOCKED (owner)**: PowerChat dashboard. Billing runs in shadow. | Billing [`docs/live-cutover.md`](https://github.com/OpenVibers/OpenVibe.Billing/blob/main/docs/live-cutover.md) | Depends on the step reached. After step 9 there is **no tool** |
| E | Money → OpenVibe.Billing | **BLOCKED (owner)**: PowerChat dashboard. Billing runs in shadow. | Billing [`docs/live-cutover.md`](https://github.com/OpenVibers/OpenVibe.Billing/blob/main/docs/live-cutover.md) | Depends on the step reached. After step 9 there is **no tool** |
-| F | RTMP ingest → OpenRe.Stream | **BLOCKED (owner)**: broadcaster windows and the per-slot rehearsal. DNS `ingest.openre.stream` is done, port 1936 is open (2026-09-23) and OpenRe is deployed. | OpenRe [`docs/cutover.md`](https://github.com/OpenVibers/OpenRe.Stream/blob/main/docs/cutover.md) (steps, checks and rollback; `scripts/cutover-preflight.js`) | Per slot `{"authority":"live"}` + `set-definition-state.js --state disabled`. All slots: unset `OPENRE_URL` |
+| F | RTMP ingest → OpenRestream | **BLOCKED (owner)**: broadcaster windows and the per-slot rehearsal. DNS `ingest.openre.stream` is done, port 1936 is open (2026-09-23) and OpenRestream is deployed. | OpenRestream [`docs/cutover.md`](https://github.com/OpenVibers/OpenRestream/blob/main/docs/cutover.md) (steps, checks and rollback; `scripts/cutover-preflight.js`) | Per slot `{"authority":"live"}` + `set-definition-state.js --state disabled`. All slots: unset `OPENRE_URL` |
| G | Retire the shims | **NOT STARTED**. Dated in [compatibility-register.md](compatibility-register.md) | Network [`docs/retirement.md`](https://github.com/OpenVibers/OpenVibe.Network/blob/main/docs/retirement.md) | per entry |
| G | Retire the shims | **NOT STARTED**. Dated in [compatibility-register.md](compatibility-register.md) | Network [`docs/retirement.md`](https://github.com/OpenVibers/OpenVibe.Network/blob/main/docs/retirement.md) | per entry |
The remaining order is D → E → F → G.
The remaining order is D → E → F → G.
- D first: nothing depends on it, and v1 can stop only when every consumer requires v2.
- D first: nothing depends on it, and v1 can stop only when every consumer requires v2.
- E before F: the Billing switch restarts Live once, and it is best done before any stream depends on
- E before F: the Billing switch restarts Live once, and it is best done before any stream depends on
- OpenRe.
+ OpenRestream.
- G follows each entry's date in the register.
- G follows each entry's date in the register.
## What the owner must do
## What the owner must do
@@ -52,9 +52,9 @@ These actions need the owner's accounts or physical presence. Nothing in this ru
@@ -52,9 +52,9 @@ These actions need the owner's accounts or physical presence. Nothing in this ru
first, and Billing is frozen to hold deliveries. A 410 from Live makes PowerChat disable the endpoint
first, and Billing is frozen to hold deliveries. A 410 from Live makes PowerChat disable the endpoint
(Billing `docs/live-cutover.md:131-134, 170-175`).
(Billing `docs/live-cutover.md:131-134, 170-175`).
2. **Cloudflare DNS, for step F.** Create an A record `ingest.openre.stream` pointing at the host's IP,
2. **Cloudflare DNS, for step F.** Create an A record `ingest.openre.stream` pointing at the host's IP,
- **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRe `README.md:145`,
+ **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRestream `README.md:145`,
`deploy/nginx/openre.stream.conf:15-17`).
`deploy/nginx/openre.stream.conf:15-17`).
-3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRe
+3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRestream
`README.md:145,160`). *Done 2026-09-23: 1936 answers from outside since 19:24 UTC.*
`README.md:145,160`). *Done 2026-09-23: 1936 answers from outside since 19:24 UTC.*
3a. **DNS changes in a cutover (any step that moves a hostname).** A day before: lower the record's TTL to 60 s
3a. **DNS changes in a cutover (any step that moves a hostname).** A day before: lower the record's TTL to 60 s
and write down the current records (`dig +noall +answer <name> A AAAA CNAME` into the step's notes). At the
and write down the current records (`dig +noall +answer <name> A AAAA CNAME` into the step's notes). At the
@@ -117,11 +117,11 @@ These rules are learned from outages. `ovhost` encodes them (see [README](../REA
@@ -117,11 +117,11 @@ These rules are learned from outages. `ovhost` encodes them (see [README](../REA
(`deploy/scripts/deploy.sh:1-42`.) It has **no automatic rollback** and does not check `/api/ready`.
(`deploy/scripts/deploy.sh:1-42`.) It has **no automatic rollback** and does not check `/api/ready`.
`apps/gateway/deploy/scripts/deploy.sh` is the superseded per-app script. Do not use it.
`apps/gateway/deploy/scripts/deploy.sh` is the superseded per-app script. Do not use it.
-- **OpenRe.Stream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.
+- **OpenRestream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.
- `api` restarts the API and coordinator only, polls `/api/ready` for 60 s, and rolls back on failure
- `api` restarts the API and coordinator only, polls `/api/ready` for 60 s, and rolls back on failure
(exit 2).
(exit 2).
- `workers` starts a new generation. The old ones drain for up to 30 min.
- `workers` starts a new generation. The old ones drain for up to 30 min.
- - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRe `README.md:147-160`.)
+ - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRestream `README.md:147-160`.)
- **Everything else** (Network, Community, Events, Chat, Billing, and the loopback services): pull,
- **Everything else** (Network, Community, Events, Chat, Billing, and the loopback services): pull,
install if the lockfile changed, restart the unit, then poll `/api/ready`. Network's own
install if the lockfile changed, restart the unit, then poll `/api/ready`. Network's own
`deploy.sh` and Sites' `deploy.sh` exist (Host `README.md`, rules table).
`deploy.sh` and Sites' `deploy.sh` exist (Host `README.md`, rules table).
@@ -316,9 +316,9 @@ PowerChat delivery lands in exactly one ledger.
@@ -316,9 +316,9 @@ PowerChat delivery lands in exactly one ledger.
Read the full list before choosing the day. VIP checkout stays closed until this cutover.
Read the full list before choosing the day. VIP checkout stays closed until this cutover.
-## F. RTMP ingest → OpenRe.Stream: BLOCKED on the owner (rehearsal and broadcaster windows; DNS and port 1936 are done)
+## F. RTMP ingest → OpenRestream: BLOCKED on the owner (rehearsal and broadcaster windows; DNS and port 1936 are done)
-OpenRe is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are
+OpenRestream is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are
`openre-session-coordinator`, `openre-rtmp-ingest@<sha>` and `openre-restream-worker@<sha>`. The database
`openre-session-coordinator`, `openre-rtmp-ingest@<sha>` and `openre-restream-worker@<sha>`. The database
is `/var/lib/openre/openre.db`. RTMP is bound to 127.0.0.1:1936 (`OPENRE_RTMP_BIND`). A loopback rehearsal
is `/var/lib/openre/openre.db`. RTMP is bound to 127.0.0.1:1936 (`OPENRE_RTMP_BIND`). A loopback rehearsal
with an ffmpeg test pattern passed: the session went live→ended, a wrong key was refused, `openre.session.*`
with an ffmpeg test pattern passed: the session went live→ended, a wrong key was refused, `openre.session.*`
@@ -326,14 +326,14 @@ reached Events, and the key never appeared in logs.
@@ -326,14 +326,14 @@ reached Events, and the key never appeared in logs.
Live carries the integration (`f0ca18b`). It is inert until `OPENRE_URL` is set. `ingest_authority`
Live carries the integration (`f0ca18b`). It is inert until `OPENRE_URL` is set. `ingest_authority`
defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP moves
defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP moves
-(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRe `README.md:207-209`).
+(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRestream `README.md:207-209`).
-**Once, before the first slot.** OpenRe's [`docs/cutover.md`](https://github.com/OpenVibers/OpenRe.Stream/blob/main/docs/cutover.md) supersedes the table below: it has the exact commands, the owner and agent steps (A1–A7), and a read-only preflight whose output on 2026-09-23 settles the "Unknown" rows (no subscription, no `OPENRE_*` in `live.env`).
+**Once, before the first slot.** OpenRestream's [`docs/cutover.md`](https://github.com/OpenVibers/OpenRestream/blob/main/docs/cutover.md) supersedes the table below: it has the exact commands, the owner and agent steps (A1–A7), and a read-only preflight whose output on 2026-09-23 settles the "Unknown" rows (no subscription, no `OPENRE_*` in `live.env`).
| Step | State |
| Step | State |
|---|---|
|---|---|
| 1. Network client `openre` and its grants; release the contract proposals | **DONE** (operator notes: principal `openre` in `openre.env`; openre capabilities released by contracts v0.19.0) |
| 1. Network client `openre` and its grants; release the contract proposals | **DONE** (operator notes: principal `openre` in `openre.env`; openre capabilities released by contracts v0.19.0) |
-| 2. Deploy OpenRe; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |
+| 2. Deploy OpenRestream; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |
| — Owner: DNS-only `ingest.openre.stream`; 1936/tcp open at the host and provider edge; RTMP bound publicly (`OPENRE_RTMP_BIND`) | **BLOCKED (owner)** |
| — Owner: DNS-only `ingest.openre.stream`; 1936/tcp open at the host and provider edge; RTMP bound publicly (`OPENRE_RTMP_BIND`) | **BLOCKED (owner)** |
| 3. Events subscription `openre.session.*` → `http://127.0.0.1:3000/internal/openre-events`; its secret in Live as `OPENRE_EVENTS_SECRET` | Unknown. Check `GET /api/v1/subscriptions` on Events and `ovhost validate live`. |
| 3. Events subscription `openre.session.*` → `http://127.0.0.1:3000/internal/openre-events`; its secret in Live as `OPENRE_EVENTS_SECRET` | Unknown. Check `GET /api/v1/subscriptions` on Events and `ovhost validate live`. |
| 4. Live: `OPENRE_URL=http://127.0.0.1:4500`, `OPENRE_PUBLIC_URL`, then `deploy.sh --wait-idle`. With no slot switched this changes nothing. | Unknown. Check `GET /api/admin/openre/status`. |
| 4. Live: `OPENRE_URL=http://127.0.0.1:4500`, `OPENRE_PUBLIC_URL`, then `deploy.sh --wait-idle`. With no slot switched this changes nothing. | Unknown. Check `GET /api/admin/openre/status`. |
@@ -356,8 +356,8 @@ defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP
@@ -356,8 +356,8 @@ defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP
Live RTMP URL.
Live RTMP URL.
- All slots: unset `OPENRE_URL` in `live.env` and restart Live (`README.md:205`,
- All slots: unset `OPENRE_URL` in `live.env` and restart Live (`README.md:205`,
`server/openre/authority.js:47-49`).
`server/openre/authority.js:47-49`).
-- Live's ingest code stays until the last protocol has run on OpenRe for two weeks (ADR-009 §Rollback).
-- Port 1935 moves to OpenRe (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening
+- Live's ingest code stays until the last protocol has run on OpenRestream for two weeks (ADR-009 §Rollback).
+- Port 1935 moves to OpenRestream (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening
(`README.md:139-145`).
(`README.md:139-145`).
## G. Retiring the shims: NOT STARTED
## G. Retiring the shims: NOT STARTED
@@ -56,7 +56,7 @@ For each of these units, `systemctl show` records `MainPID`, `InvocationID`, `Ac
@@ -56,7 +56,7 @@ For each of these units, `systemctl show` records `MainPID`, `InvocationID`, `Ac
`ExecMainStartTimestamp`, `NRestarts`, `ActiveState` and `SubState`:
`ExecMainStartTimestamp`, `NRestarts`, `ActiveState` and `SubState`:
- every unit and socket in the inventory except sources' own unit;
- every unit and socket in the inventory except sources' own unit;
-- every `openvibe-*` and `openre-*` unit (for example OpenRe's transport worker instances);
+- every `openvibe-*` and `openre-*` unit (for example OpenRestream's transport worker instances);
- `nginx.service`;
- `nginx.service`;
- always, even if not loaded: `openvibe-live.socket`, `openvibe-live.service`,
- always, even if not loaded: `openvibe-live.socket`, `openvibe-live.service`,
`openvibe-chat.service` and `openvibe-media.service`;
`openvibe-chat.service` and `openvibe-media.service`;
@@ -24,7 +24,7 @@ root (`sudo ovhost data …`; `--dry-run` need not): the scripts need it, and an
@@ -24,7 +24,7 @@ root (`sudo ovhost data …`; `--dry-run` need not): the scripts need it, and an
`VALKEY_PREFIX` — never a value.
`VALKEY_PREFIX` — never a value.
- **`data switch`** runs `roles/data/switch-service.sh <service> [<file>]`. It takes `SWITCH_UNITS` and
- **`data switch`** runs `roles/data/switch-service.sh <service> [<file>]`. It takes `SWITCH_UNITS` and
`SWITCH_DIR` from the inventory entry (`units` plus `workerUnits`, a template worker shown as a glob
`SWITCH_DIR` from the inventory entry (`units` plus `workerUnits`, a template worker shown as a glob
- such as `openre-rtmp-ingest@*.service`, plus the `unitsMatch` glob when set; `repo`), so a release-layout service such as OpenRe needs no
+ such as `openre-rtmp-ingest@*.service`, plus the `unitsMatch` glob when set; `repo`), so a release-layout service such as OpenRestream needs no
hand-set environment. `--sqlite <file>` names the SQLite file when it is not
hand-set environment. `--sqlite <file>` names the SQLite file when it is not
`/var/lib/openvibe-<service>/<service>.db` (Host: `/var/lib/openvibe-host-api/host.db`). Before running
`/var/lib/openvibe-<service>/<service>.db` (Host: `/var/lib/openvibe-host-api/host.db`). Before running
it, the service's postgres branch must be merged with green CI and its auto-deploy frozen
it, the service's postgres branch must be merged with green CI and its auto-deploy frozen
@@ -38,7 +38,7 @@ logical `.dump` feeds the drills ([backups.md](backups.md#postgresql-services)).
@@ -38,7 +38,7 @@ logical `.dump` feeds the drills ([backups.md](backups.md#postgresql-services)).
keeps its string form, and PostgreSQL is the object `{ "engine": "postgresql", "database": "ov_host" }`.
keeps its string form, and PostgreSQL is the object `{ "engine": "postgresql", "database": "ov_host" }`.
- the `protected` probe kind `postgresql-count` — `{ "kind": "postgresql-count", "database": "ov_openre",
- the `protected` probe kind `postgresql-count` — `{ "kind": "postgresql-count", "database": "ov_openre",
"sql": "SELECT count(*) …", "label": "…" }`: a read-only SELECT against a database in the cluster, run as
"sql": "SELECT count(*) …", "label": "…" }`: a read-only SELECT against a database in the cluster, run as
- the postgres OS user. OpenRe's protected probe (its active ingest sessions) is one, on `ov_openre`. The
+ the postgres OS user. OpenRestream's protected probe (its active ingest sessions) is one, on `ov_openre`. The
other kinds are `http-json-count`, `sqlite-count` and `sum` ([lib/probes.js](../lib/probes.js)).
other kinds are `http-json-count`, `sqlite-count` and `sum` ([lib/probes.js](../lib/probes.js)).
---
---
@@ -8,7 +8,7 @@ Each proof is a recorded production run: what was deployed, how it was observed,
@@ -8,7 +8,7 @@ Each proof is a recorded production run: what was deployed, how it was observed,
| Recorder / media-worker checkpoint (Media) | open | needs a live ingest during a Media deploy |
| Recorder / media-worker checkpoint (Media) | open | needs a live ingest during a Media deploy |
| Chat resume (Chat) | passed 2026-09-26, with a limit | [below](#chat-resume-chat-2026-09-26) |
| Chat resume (Chat) | passed 2026-09-26, with a limit | [below](#chat-resume-chat-2026-09-26) |
| Game shard (Games) | passed 2026-09-26 | [below](#game-shard-games-2026-09-26) |
| Game shard (Games) | passed 2026-09-26 | [below](#game-shard-games-2026-09-26) |
-| Ingest worker (OpenRe) | after the cutover (WS-H) | |
+| Ingest worker (OpenRestream) | after the cutover (WS-H) | |
The release-lifecycle acceptance suite ([release-acceptance.md](release-acceptance.md)) reads this table. The state of a row and the numbers quoted from its record are gates `6f`, `6g`, `6h` and `7e`: a row that says `passed` must still contain those numbers, and an `open` row stays open there.
The release-lifecycle acceptance suite ([release-acceptance.md](release-acceptance.md)) reads this table. The state of a row and the numbers quoted from its record are gates `6f`, `6g`, `6h` and `7e`: a row that says `passed` must still contain those numbers, and an `open` row stays open there.
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
# Deploy strategies (roadmap WS-N task 11)
# Deploy strategies (roadmap WS-N task 11)
`ovhost deploy <svc>` now covers the services that deployed with their own `deploy/scripts/deploy.sh`:
`ovhost deploy <svc>` now covers the services that deployed with their own `deploy/scripts/deploy.sh`:
-Live (release layout), Tools, Sites, OpenRe and Network, and Games (which deployed by hand). Each service
+Live (release layout), Tools, Sites, OpenRestream and Network, and Games (which deployed by hand). Each service
entry in the inventory names a **strategy**: an engine plus the defaults the script it replaces encoded.
entry in the inventory names a **strategy**: an engine plus the defaults the script it replaces encoded.
Every repository keeps `deploy/scripts/deploy.sh` as a thin wrapper that maps its old flags onto ovhost,
Every repository keeps `deploy/scripts/deploy.sh` as a thin wrapper that maps its old flags onto ovhost,
with the old script body kept as `deploy/scripts/deploy-legacy.sh` for the fallback.
with the old script body kept as `deploy/scripts/deploy-legacy.sh` for the fallback.
@@ -15,7 +15,7 @@ reached where an ovhost without `capabilities` answers, which no longer happens
@@ -15,7 +15,7 @@ reached where an ovhost without `capabilities` answers, which no longer happens
| Service | Strategy | Replaces | Wrapper (`deploy/scripts/deploy.sh`) |
| Service | Strategy | Replaces | Wrapper (`deploy/scripts/deploy.sh`) |
|---|---|---|---|
|---|---|---|---|
| live | `release-layout` | Live `deploy.sh` (release layout) | `--wait-idle`, `--restart`, `--rollback` → `ovhost rollback live`, `DRY_RUN=1` → `ovhost plan live`; `--force` refused (see below) |
| live | `release-layout` | Live `deploy.sh` (release layout) | `--wait-idle`, `--restart`, `--rollback` → `ovhost rollback live`, `DRY_RUN=1` → `ovhost plan live`; `--force` refused (see below) |
-| openre | `release-layout` | OpenRe `deploy.sh release` + `api` | `deploy`, `release [<ref>]` → `--prepare-only`, `api [<sha>]` → `deploy --to <sha>`, `rollback`, `plan`; `workers`, `status`, `prune` stay in the legacy script |
+| openre | `release-layout` | OpenRestream `deploy.sh release` + `api` | `deploy`, `release [<ref>]` → `--prepare-only`, `api [<sha>]` → `deploy --to <sha>`, `rollback`, `plan`; `workers`, `status`, `prune` stay in the legacy script |
| tools | `multi-app` | Tools `deploy.sh` | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |
| tools | `multi-app` | Tools `deploy.sh` | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |
| sites | `static-build` | Sites `deploy.sh` (after a manual pull) | always `ovhost deploy sites --restart` (rebuild every run, as before); `DRY_RUN=1` |
| sites | `static-build` | Sites `deploy.sh` (after a manual pull) | always `ovhost deploy sites --restart` (rebuild every run, as before); `DRY_RUN=1` |
| games | `pnpm-build` | the manual procedure | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |
| games | `pnpm-build` | the manual procedure | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |
@@ -47,7 +47,7 @@ original arguments. So an old ovhost, a missing one, or an inventory that has no
@@ -47,7 +47,7 @@ original arguments. So an old ovhost, a missing one, or an inventory that has no
breaks a deploy. `OVHOST_LEGACY=1` forces the fallback; `OVHOST=<path>` picks another ovhost. Where the legacy
breaks a deploy. `OVHOST_LEGACY=1` forces the fallback; `OVHOST=<path>` picks another ovhost. Where the legacy
script cannot do what was asked (Tools, Network and Games have no `--rollback`; none of them has `DRY_RUN`
script cannot do what was asked (Tools, Network and Games have no `--rollback`; none of them has `DRY_RUN`
except Live), the wrapper refuses instead of doing something else. The Sites fallback pulls first (it restores
except Live), the wrapper refuses instead of doing something else. The Sites fallback pulls first (it restores
-`dist/` and runs `git pull --ff-only` as the checkout owner), then runs the pulled legacy script. OpenRe's
+`dist/` and runs `git pull --ff-only` as the checkout owner), then runs the pulled legacy script. OpenRestream's
fallback runs `release` then `api` for `deploy`.
fallback runs `release` then `api` for `deploy`.
Exit codes are ovhost's: `0` ok · `1` usage/precondition · `2` validation failed, nothing restarted · `3` not
Exit codes are ovhost's: `0` ok · `1` usage/precondition · `2` validation failed, nothing restarted · `3` not
@@ -103,7 +103,7 @@ mean. (In the fallback, `--force` goes to the legacy script as before.)
@@ -103,7 +103,7 @@ mean. (In the fallback, `--force` goes to the legacy script as before.)
reasons). After readiness a listener that still is not systemd's is reported (`✗ socket activation is not
reasons). After readiness a listener that still is not systemd's is reported (`✗ socket activation is not
in effect`) and recorded (`socketHeld: false`).
in effect`) and recorded (`socketHeld: false`).
6. Prune to `release.keep`: never `current`, never the release just left, never one a worker unit instance
6. Prune to `release.keep`: never `current`, never the release just left, never one a worker unit instance
- runs from (OpenRe's `openre-rtmp-ingest@<id>`).
+ runs from (OpenRestream's `openre-rtmp-ingest@<id>`).
7. Record (`fromRelease`, `toRelease`), announce.
7. Record (`fromRelease`, `toRelease`), announce.
`ovhost rollback <svc>` switches back to the release the release log says the current one replaced (or
`ovhost rollback <svc>` switches back to the release the release log says the current one replaced (or
@@ -120,10 +120,10 @@ overrides them.
@@ -120,10 +120,10 @@ overrides them.
| `strategy` | `git-checkout`, `multi-app`, `static-build`, `pnpm-build` or `release-layout` | `git-checkout`; an entry with `layout: "release"` and no strategy stays unmanaged (deployed by its own script), as before |
| `strategy` | `git-checkout`, `multi-app`, `static-build`, `pnpm-build` or `release-layout` | `git-checkout`; an entry with `layout: "release"` and no strategy stays unmanaged (deployed by its own script), as before |
| `release.git` | the clone releases are made from (relative to `repo`, or absolute inside it) | `repo` |
| `release.git` | the clone releases are made from (relative to `repo`, or absolute inside it) | `repo` |
| `release.releases`, `release.current` | the releases directory and the link | `releases`, `current` |
| `release.releases`, `release.current` | the releases directory and the link | `releases`, `current` |
-| `release.id` | `time-sha8` (`<UTC yyyymmdd-HHMMSS>-<sha8>`, Live; the old script used the host's local time, the same on a UTC host) or `sha12` (OpenRe; one release per commit, reused) | `time-sha8` |
+| `release.id` | `time-sha8` (`<UTC yyyymmdd-HHMMSS>-<sha8>`, Live; the old script used the host's local time, the same on a UTC host) or `sha12` (OpenRestream; one release per commit, reused) | `time-sha8` |
| `release.links` | `{ name: target }` links made in every release | `{}` |
| `release.links` | `{ name: target }` links made in every release | `{}` |
| `release.reuseModules` | hard-link the current `node_modules` when dependencies did not change | `true` |
| `release.reuseModules` | hard-link the current `node_modules` when dependencies did not change | `true` |
-| `release.chown` | `user[:group]` for each new release (OpenRe) | none |
+| `release.chown` | `user[:group]` for each new release (OpenRestream) | none |
| `release.keep` | releases kept (2–50) | 5 |
| `release.keep` | releases kept (2–50) | 5 |
| `release.settleSeconds` | wait after a switch without restart before the ready check | 3 |
| `release.settleSeconds` | wait after a switch without restart before the ready check | 3 |
| `release.socketPort` | the port pid 1 must hold | the service `port` |
| `release.socketPort` | the port pid 1 must hold | the service `port` |
@@ -172,13 +172,13 @@ itself (`/opt/openvibe.live/current`) is read the same way. `status`, `validate`
@@ -172,13 +172,13 @@ itself (`/opt/openvibe.live/current`) is read the same way. `status`, `validate`
| Sites: pull BEFORE running | ovhost fetches and merges first by construction |
| Sites: pull BEFORE running | ovhost fetches and merges first by construction |
| Sites: `npm ci`, `node build.js`, vhosts, `nginx -t` then reload, never reload after a failed test | static-build defaults; `nginx.install` restores the previous files |
| Sites: `npm ci`, `node build.js`, vhosts, `nginx -t` then reload, never reload after a failed test | static-build defaults; `nginx.install` restores the previous files |
| Sites: one announce per placeholder, stop at the first failure | `announce.releaseFiles` |
| Sites: one announce per placeholder, stop at the first failure | `announce.releaseFiles` |
-| OpenRe: `release` (worktree, `npm ci`, chown ubuntu), `api` (switch, restart API + coordinator, 60 s ready, roll back) | release-layout with `release.id: sha12`, `reuseModules: false`, `chown`; `--prepare-only` |
-| OpenRe: never touch a worker; `prune` keeps what runs | `workerUnits` are never restarted; pruning skips releases a worker instance runs from |
+| OpenRestream: `release` (worktree, `npm ci`, chown ubuntu), `api` (switch, restart API + coordinator, 60 s ready, roll back) | release-layout with `release.id: sha12`, `reuseModules: false`, `chown`; `--prepare-only` |
+| OpenRestream: never touch a worker; `prune` keeps what runs | `workerUnits` are never restarted; pruning skips releases a worker instance runs from |
| Network: pull, `npm install` on a lockfile change, unit file installed when it differs, restart, health | git-checkout with `installUnits` |
| Network: pull, `npm install` on a lockfile change, unit file installed when it differs, restart, health | git-checkout with `installUnits` |
| Games: `git pull`, `pnpm install --frozen-lockfile`, `pnpm build`, restart | pnpm-build (git as the owner, not `sudo git`) |
| Games: `git pull`, `pnpm install --frozen-lockfile`, `pnpm build`, restart | pnpm-build (git as the owner, not `sudo git`) |
Stricter than the scripts, on purpose: Tools and Network roll back automatically and check every dependency;
Stricter than the scripts, on purpose: Tools and Network roll back automatically and check every dependency;
-Tools checks `/api/ready` (not `/api/health`) and the new sha; OpenRe refuses an API restart while an ingest
+Tools checks `/api/ready` (not `/api/health`) and the new sha; OpenRestream refuses an API restart while an ingest
session is open (the script did not check; `--wait-idle` holds, `--force` goes ahead; if the restart provably
session is open (the script did not check; `--wait-idle` holds, `--force` goes ahead; if the restart provably
never cuts a session, set its `drain.policy` to `report`); `--wait-idle` now also holds a `report`-policy
never cuts a session, set its `drain.policy` to `report`); `--wait-idle` now also holds a `report`-policy
service (Tools jobs, Games players, Events SSE) instead of being ignored; every attempt is in the release
service (Tools jobs, Games players, Events SSE) instead of being ignored; every attempt is in the release
@@ -194,7 +194,7 @@ log and a freeze refuses.
@@ -194,7 +194,7 @@ log and a freeze refuses.
2026-09-24; an in-place Live would be `strategy: git-checkout`.
2026-09-24; an in-place Live would be `strategy: git-checkout`.
- **Live `--force`** (discard local tracked changes): ovhost never discards changes (see above).
- **Live `--force`** (discard local tracked changes): ovhost never discards changes (see above).
- **Network's "restart anyway" when nothing is new**: only with `--restart` now.
- **Network's "restart anyway" when nothing is new**: only with `--restart` now.
-- **OpenRe `workers`, `status`, `prune`**: they stay in the repository's script. Starting a worker
+- **OpenRestream `workers`, `status`, `prune`**: they stay in the repository's script. Starting a worker
generation is not a deploy ovhost makes (it never starts, stops or restarts a worker unit); ovhost prunes
generation is not a deploy ovhost makes (it never starts, stops or restarts a worker unit); ovhost prunes
on its own and keeps every release a worker runs from.
on its own and keeps every release a worker runs from.
- **Games `sudo git`**: ovhost runs git as the checkout owner. If earlier `sudo git pull`s left root-owned
- **Games `sudo git`**: ovhost runs git as the checkout owner. If earlier `sudo git pull`s left root-owned
@@ -238,7 +238,7 @@ read-only until step 5.
@@ -238,7 +238,7 @@ read-only until step 5.
entry only gains `installUnits`).
entry only gains `installUnits`).
5. **Pull the wrappers.** Tools, Sites, Network and Games get theirs with their next pull (the old Tools and
5. **Pull the wrappers.** Tools, Sites, Network and Games get theirs with their next pull (the old Tools and
Network scripts pull themselves, so the deploy that brings the wrapper is still run by the old script).
Network scripts pull themselves, so the deploy that brings the wrapper is still run by the old script).
- Live's arrives in the release that contains it; OpenRe's with `git -C /opt/openre.stream/repo pull`.
+ Live's arrives in the release that contains it; OpenRestream's with `git -C /opt/openre.stream/repo pull`.
6. **Deploy through the wrapper**, one service at a time, lowest risk first: network, sites, games (quiet
6. **Deploy through the wrapper**, one service at a time, lowest risk first: network, sites, games (quiet
hour or `--wait-idle`), tools, openre (no ingest session, or `--wait-idle`), live (`--wait-idle`). Each
hour or `--wait-idle`), tools, openre (no ingest session, or `--wait-idle`), live (`--wait-idle`). Each
wrapper prints `ovhost deploy <svc> …`; if it prints "running deploy-legacy.sh", read why.
wrapper prints `ovhost deploy <svc> …`; if it prints "running deploy-legacy.sh", read why.
@@ -9,7 +9,7 @@ Public listeners on 2026-09-26, all intended:
@@ -9,7 +9,7 @@ Public listeners on 2026-09-26, all intended:
| 22 | tcp | sshd |
| 22 | tcp | sshd |
| 80, 443 | tcp | nginx |
| 80, 443 | tcp | nginx |
| 1935 | tcp | Live RTMP ingest (DNS-only `ingest.openvibe.live`) |
| 1935 | tcp | Live RTMP ingest (DNS-only `ingest.openvibe.live`) |
-| 1936 | tcp | OpenRe RTMP ingest |
+| 1936 | tcp | OpenRestream RTMP ingest |
| 3478, 5349 | tcp + udp | coturn |
| 3478, 5349 | tcp + udp | coturn |
| 49152–65535 | udp | coturn relay range (`min-port`/`max-port`) |
| 49152–65535 | udp | coturn relay range (`min-port`/`max-port`) |
| 9710–9789 | tcp | nginx JSMPEG ports (bound to the public address) |
| 9710–9789 | tcp | nginx JSMPEG ports (bound to the public address) |
@@ -133,7 +133,7 @@ The tab's EventSource is subject to the site's Content-Security-Policy, so `conn
@@ -133,7 +133,7 @@ The tab's EventSource is subject to the site's Content-Security-Policy, so `conn
`https://openvibe.events`. As of 2026-09-26, every site whose pages load release-watch under a CSP
`https://openvibe.events`. As of 2026-09-26, every site whose pages load release-watch under a CSP
allows it: Live and Community first, then, with their 1.17.1 pins, Network, Wiki, Blog, Codes, Search, Reviews,
allows it: Live and Community first, then, with their 1.17.1 pins, Network, Wiki, Blog, Codes, Search, Reviews,
VIP, Tips, Host (Stage B dashboard), News, Deals, Coupons, Trade and the Tools apps. Chat, Media, Games,
VIP, Tips, Host (Stage B dashboard), News, Deals, Coupons, Trade and the Tools apps. Chat, Media, Games,
-OpenRe.Stream and Sites send no CSP with those pages. A new site needs it too. Without it, the
+OpenRestream and Sites send no CSP with those pages. A new site needs it too. Without it, the
browser refuses the stream and logs one CSP error per page load (the browser check counts it).
browser refuses the stream and logs one CSP error per page load (the browser check counts it).
release-watch then stops, with state `blocked`, and keeps polling. Add the origin in the same change that
release-watch then stops, with state `blocked`, and keeps polling. Add the origin in the same change that
moves the pin, or set `data-events="off"` on the `ov-release` meta tag.
moves the pin, or set `data-events="off"` on the `ov-release` meta tag.
@@ -202,13 +202,13 @@ Supported with declared overrides (the table above records which have passed on
@@ -202,13 +202,13 @@ Supported with declared overrides (the table above records which have passed on
- coupons
- coupons
- trade
- trade
- codes
- codes
-- openre (not run yet). Only openre-api starts (`drill.command`), with `OPENRE_DRILL=1`. From OpenRe
+- openre (not run yet). Only openre-api starts (`drill.command`), with `OPENRE_DRILL=1`. From OpenRestream
`5e86ea7` on, that makes it read-only: writes, `/play/` and sign-in answer 503. The coordinator
`5e86ea7` on, that makes it read-only: writes, `/play/` and sign-in answer 503. The coordinator
and every transport worker refuse to start in a drill, and the event relay and Media calls are
and every transport worker refuse to start in a drill, and the event relay and Media calls are
off. The API never runs a transport or the relay in any case. Empty `EVENTS_URL` and
off. The API never runs a transport or the relay in any case. Empty `EVENTS_URL` and
`OV_OAUTH_CLIENT_SECRET`, plus `MEDIA_URL` on the closed port, cover older releases. The workers
`OV_OAUTH_CLIENT_SECRET`, plus `MEDIA_URL` on the closed port, cover older releases. The workers
stay out of every ovhost command: they are `workerUnits`, which are listed and never restarted.
stay out of every ovhost command: they are `workerUnits`, which are listed and never restarted.
- Run it after the OpenRe release with `OPENRE_DRILL` is deployed (OpenRe `docs/cutover.md`
+ Run it after the OpenRestream release with `OPENRE_DRILL` is deployed (OpenRestream `docs/cutover.md`
phase C).
phase C).
- live (not run yet). Needs OpenVibe.Live `8a58aea` or later; `drill.requires` refuses an older
- live (not run yet). Needs OpenVibe.Live `8a58aea` or later; `drill.requires` refuses an older
checkout. The production unit's ExecStart starts with `LIVE_DRILL=1`, `DB_PATH` on the copy and
checkout. The production unit's ExecStart starts with `LIVE_DRILL=1`, `DB_PATH` on the copy and
@@ -82,10 +82,10 @@ Sources:
@@ -82,10 +82,10 @@ Sources:
- Games `/healthz` (the inventory now uses `/api/ready`, 2026-09-23; Network, Media and Tools too).
- Games `/healthz` (the inventory now uses `/api/ready`, 2026-09-23; Network, Media and Tools too).
The inventory should point at `/api/ready` where one exists. Games has none.
The inventory should point at `/api/ready` where one exists. Games has none.
-4. *(Fixed: the inventory has had AI and OpenRe entries since `f0dbd0c`, and both are in the scheduled
+4. *(Fixed: the inventory has had AI and OpenRestream entries since `f0dbd0c`, and both are in the scheduled
`ovhost backup --all` with off-host copies, [backups.md](backups.md). Their drills are still unsupported.)*
`ovhost backup --all` with off-host copies, [backups.md](backups.md). Their drills are still unsupported.)*
**Two services have no ovhost inventory entry, so no `ovhost backup` and no drill**: AI (`ai.db`) and
**Two services have no ovhost inventory entry, so no `ovhost backup` and no drill**: AI (`ai.db`) and
- OpenRe.Stream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron
+ OpenRestream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron
and timers.
and timers.
5. **The inventory's Tools backup list is incomplete.** It names the seven `analytics.db` files only.
5. **The inventory's Tools backup list is incomplete.** It names the seven `analytics.db` files only.
The job databases `apps/{img,audio,docs}/data/jobs.db` (table `tool_jobs`,
The job databases `apps/{img,audio,docs}/data/jobs.db` (table `tool_jobs`,
@@ -132,7 +132,7 @@ reviewed on 2026-09-23.
@@ -132,7 +132,7 @@ reviewed on 2026-09-23.
| **Community** | `/var/lib/openvibe-community/community.db` (`StateDirectory`, `deploy/systemd/openvibe-community.service:26-27`). `/opt/openvibe.community/data/community.db` is the legacy location, and the inventory keeps whichever of the two exists. `COMMUNITY_DB_PATH`, default `./data/community.db` (`server/config.js:56`). | Yes: pastes (`PASTES_AUTHORITY=community` since 2026-09-22), comments, spaces, threads, Pulse | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:29-66`). Required: `db` (`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`, fails on 0). Optional: `network_jwks`, `media` (community mode) or `live` (live mode). Reports `pastes_authority`. | **Yes** | Drill passed 03:44 (manual) and 05:22. |
| **Community** | `/var/lib/openvibe-community/community.db` (`StateDirectory`, `deploy/systemd/openvibe-community.service:26-27`). `/opt/openvibe.community/data/community.db` is the legacy location, and the inventory keeps whichever of the two exists. `COMMUNITY_DB_PATH`, default `./data/community.db` (`server/config.js:56`). | Yes: pastes (`PASTES_AUTHORITY=community` since 2026-09-22), comments, spaces, threads, Pulse | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:29-66`). Required: `db` (`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`, fails on 0). Optional: `network_jwks`, `media` (community mode) or `live` (live mode). Reports `pastes_authority`. | **Yes** | Drill passed 03:44 (manual) and 05:22. |
| **Events** | `/var/lib/openvibe-events/events.db` (`deploy/systemd/openvibe-events.service:30-31`); `EVENTS_DB_PATH`, default `./data/events.db` (`server/config.js:67`). | Yes: the durable event store, subscriptions, deliveries, DLQ | SQLite, the reviewed state. `README.md:163`: "the plan's PostgreSQL + Redis fanout is a later step". No dated plan. | `/api/ready` (`server/app.js:41-66`). Required: `db` (`store.ping()` = `SELECT 1`), `network_jwks`, `delivery_worker` (unless `EVENTS_WORKER=off`). Optional: `dlq` (degraded at more than `EVENTS_DLQ_DEGRADED_AT`, default 100). | **Yes** | Drill passed 05:22. |
| **Events** | `/var/lib/openvibe-events/events.db` (`deploy/systemd/openvibe-events.service:30-31`); `EVENTS_DB_PATH`, default `./data/events.db` (`server/config.js:67`). | Yes: the durable event store, subscriptions, deliveries, DLQ | SQLite, the reviewed state. `README.md:163`: "the plan's PostgreSQL + Redis fanout is a later step". No dated plan. | `/api/ready` (`server/app.js:41-66`). Required: `db` (`store.ping()` = `SELECT 1`), `network_jwks`, `delivery_worker` (unless `EVENTS_WORKER=off`). Optional: `dlq` (degraded at more than `EVENTS_DLQ_DEGRADED_AT`, default 100). | **Yes** | Drill passed 05:22. |
| **Chat** | `/var/lib/openvibe-chat/chat.db` (`deploy/systemd/openvibe-chat.service:38-39`); `CHAT_DB_PATH`, default `./data/chat.db` (`server/config.js:34`). It also reads Live's `/opt/openvibe.live/data/sounds` (`:43`). | Yes, since 2026-09-23 02:03 UTC: the 12 chat tables (`table_authority = 'chat'`). Six staged tables are still Live's. | SQLite, the reviewed state. | `/ready` (loopback). Since `45f2b1f`: required `db` (`SELECT MAX(id) FROM chat_messages`, 503 on failure); optional `live_sync` (degraded when no clean Live sync pass for `LIVE_SYNC_STALE_MS`, 60 s, or one step is late). `mirror.pending` is still reported only. | **Yes** | Drill passed 05:22. |
| **Chat** | `/var/lib/openvibe-chat/chat.db` (`deploy/systemd/openvibe-chat.service:38-39`); `CHAT_DB_PATH`, default `./data/chat.db` (`server/config.js:34`). It also reads Live's `/opt/openvibe.live/data/sounds` (`:43`). | Yes, since 2026-09-23 02:03 UTC: the 12 chat tables (`table_authority = 'chat'`). Six staged tables are still Live's. | SQLite, the reviewed state. | `/ready` (loopback). Since `45f2b1f`: required `db` (`SELECT MAX(id) FROM chat_messages`, 503 on failure); optional `live_sync` (degraded when no clean Live sync pass for `LIVE_SYNC_STALE_MS`, 60 s, or one step is late). `mirror.pending` is still reported only. | **Yes** | Drill passed 05:22. |
-| **OpenRe.Stream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |
+| **OpenRestream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |
| **Billing** | `/var/lib/openvibe-billing/billing.db` (`deploy/systemd/openvibe-billing.service:23-25`, `StateDirectoryMode=0700`); `BILLING_DB_PATH`, default `./data/billing.db` (`server/config.js:62`). The journal is append-only (triggers refuse UPDATE and DELETE, `README.md:45-50`). | **Not yet.** It is a shadow: `STATUS.json` `"authoritative": false`. Live holds the money until the cutover. | SQLite, the reviewed state. ADR-007's SERIALIZABLE and row-lock rule applies when money moves to PostgreSQL. No plan yet. | `/api/ready` (`server/app.js:54-60`, own handler). `SELECT 1 FROM settings WHERE id = 1` and Network public key loaded, otherwise 503 `service.not_ready`. `/api/health` reports frozen state and providers. | **Yes** | Drill passed 05:22. |
| **Billing** | `/var/lib/openvibe-billing/billing.db` (`deploy/systemd/openvibe-billing.service:23-25`, `StateDirectoryMode=0700`); `BILLING_DB_PATH`, default `./data/billing.db` (`server/config.js:62`). The journal is append-only (triggers refuse UPDATE and DELETE, `README.md:45-50`). | **Not yet.** It is a shadow: `STATUS.json` `"authoritative": false`. Live holds the money until the cutover. | SQLite, the reviewed state. ADR-007's SERIALIZABLE and row-lock rule applies when money moves to PostgreSQL. No plan yet. | `/api/ready` (`server/app.js:54-60`, own handler). `SELECT 1 FROM settings WHERE id = 1` and Network public key loaded, otherwise 503 `service.not_ready`. `/api/health` reports frozen state and providers. | **Yes** | Drill passed 05:22. |
| **Tips** | `/var/lib/openvibe-tips/tips.db` (`deploy/systemd/openvibe-tips.service:25`); `TIPS_DB_PATH` (`server/config.js:31`). | Yes: creator tip profiles, interactions, goals | SQLite, the reviewed state. | `/api/ready` (`server/app.js:74-79`, own handler): `SELECT 1 FROM settings WHERE id = 1` and key loaded. | **Yes** | Drill passed 05:23. |
| **Tips** | `/var/lib/openvibe-tips/tips.db` (`deploy/systemd/openvibe-tips.service:25`); `TIPS_DB_PATH` (`server/config.js:31`). | Yes: creator tip profiles, interactions, goals | SQLite, the reviewed state. | `/api/ready` (`server/app.js:74-79`, own handler): `SELECT 1 FROM settings WHERE id = 1` and key loaded. | **Yes** | Drill passed 05:23. |
| **VIP** | `/var/lib/openvibe-vip/vip.db` (`deploy/systemd/openvibe-vip.service:25`); `VIP_DB_PATH` (`server/config.js:31`). | Yes: creators, plans, memberships, perks (checkout closed until the Billing cutover) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:20-48`). Required: `db` (`COUNT(*) FROM vip_creators >= 1`). Optional: `network_jwks`, `billing`. | **Yes** | Drill passed 05:23. |
| **VIP** | `/var/lib/openvibe-vip/vip.db` (`deploy/systemd/openvibe-vip.service:25`); `VIP_DB_PATH` (`server/config.js:31`). | Yes: creators, plans, memberships, perks (checkout closed until the Billing cutover) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:20-48`). Required: `db` (`COUNT(*) FROM vip_creators >= 1`). Optional: `network_jwks`, `billing`. | **Yes** | Drill passed 05:23. |
@@ -152,11 +152,11 @@ reviewed on 2026-09-23.
@@ -152,11 +152,11 @@ reviewed on 2026-09-23.
## Deployed-state notes
## Deployed-state notes
- **Loopback only**, not on their domains yet (operator notes): Tips, VIP, Search, Sources, News, Reviews,
- **Loopback only**, not on their domains yet (operator notes): Tips, VIP, Search, Sources, News, Reviews,
- Codes, AI and OpenRe's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:
+ Codes, AI and OpenRestream's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:
Billing. Deals, Coupons and Trade passed drills against production, so they run on the host. Whether
Billing. Deals, Coupons and Trade passed drills against production, so they run on the host. Whether
they are public is unknown: check nginx `sites-enabled`. Sites still has placeholders for
they are public is unknown: check nginx `sites-enabled`. Sites still has placeholders for
openvibe.deals, .coupons and .trade.
openvibe.deals, .coupons and .trade.
-- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRe
+- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRestream
and AI still say `"deployed": false`, and so do Tips, Search, Sources, News, Reviews, Deals, Coupons,
and AI still say `"deployed": false`, and so do Tips, Search, Sources, News, Reviews, Deals, Coupons,
Trade and Codes. `STATUS.json` stays each service's own record to fix. This page does not change it.
Trade and Codes. `STATUS.json` stays each service's own record to fix. This page does not change it.
@@ -2272,7 +2272,7 @@
@@ -2272,7 +2272,7 @@
"table": "migration_map"
"table": "migration_map"
}
}
],
],
- "_notes": "Only openre-api starts (command; the working directory and the non-secret Environment= come from openre-api.service). It never runs a transport, a coordinator or the event relay in any mode. OPENRE_DRILL=1 (OpenRe 5e86ea7 and later) also makes it read-only: writes, /play/ (which would pull from production's worker on loopback) and sign-in (which would redeem codes at Network) answer 503. The coordinator and the workers refuse to start. The event relay and Media calls are off. EVENTS_URL and OV_OAUTH_CLIENT_SECRET empty and MEDIA_URL on the closed port are the same switches for releases before 5e86ea7. Left on (read-only): the Network JWKS GET on loopback. Not compared: ingest_sessions and workers (they change whenever someone streams or a generation rolls over) and /api/ready (it reports live worker heartbeats)."
+ "_notes": "Only openre-api starts (command; the working directory and the non-secret Environment= come from openre-api.service). It never runs a transport, a coordinator or the event relay in any mode. OPENRE_DRILL=1 (OpenRestream 5e86ea7 and later) also makes it read-only: writes, /play/ (which would pull from production's worker on loopback) and sign-in (which would redeem codes at Network) answer 503. The coordinator and the workers refuse to start. The event relay and Media calls are off. EVENTS_URL and OV_OAUTH_CLIENT_SECRET empty and MEDIA_URL on the closed port are the same switches for releases before 5e86ea7. Left on (read-only): the Network JWKS GET on loopback. Not compared: ingest_sessions and workers (they change whenever someone streams or a generation rolls over) and /api/ready (it reports live worker heartbeats)."
},
},
"protected": {
"protected": {
"kind": "postgresql-count",
"kind": "postgresql-count",
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
*
*
* ovhost deploy|rollback <service> announces after a deploy or rollback that went live
* ovhost deploy|rollback <service> announces after a deploy or rollback that went live
* ovhost announce <service> for services deployed by their own scripts (Live, Tools, Sites,
* ovhost announce <service> for services deployed by their own scripts (Live, Tools, Sites,
- * OpenRe, Games)
+ * OpenRestream, Games)
*
*
* Best effort: it never fails or holds up a deploy. The token request and the publish each have a
* Best effort: it never fails or holds up a deploy. The token request and the publish each have a
* short timeout and the whole announcement a budget of a few seconds; a failure is logged and the
* short timeout and the whole announcement a budget of a few seconds; a failure is logged and the
@@ -35,7 +35,7 @@
@@ -35,7 +35,7 @@
* writes to the checkout, the env file, the database or the backup. It never reads an env value:
* writes to the checkout, the env file, the database or the backup. It never reads an env value:
* systemd loads the production env file for the instance, and the override file holds only
* systemd loads the production env file for the instance, and the override file holds only
* inventory values. Environment= values from the unit are passed on only if their names do not look
* inventory values. Environment= values from the unit are passed on only if their names do not look
- * secret and the drill does not set them itself (OpenRe's unit sets DATABASE_URL to production's
+ * secret and the drill does not set them itself (OpenRestream's unit sets DATABASE_URL to production's
* database: that line never reaches the drill instance).
* database: that line never reaches the drill instance).
*
*
* The production env file is loaded FIRST, so any variable the drill does not set keeps its
* The production env file is loaded FIRST, so any variable the drill does not set keeps its
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
* that break the next pull.
* that break the next pull.
*/
*/
function git(exec, svc, { dir = svc.repo, safeDirectory = false } = {}) {
function git(exec, svc, { dir = svc.repo, safeDirectory = false } = {}) {
- // A release directory may belong to the service user while git runs as the clone's owner (OpenRe
+ // A release directory may belong to the service user while git runs as the clone's owner (OpenRestream
// chowns each release): -c safe.directory names that one directory, nothing wider.
// chowns each release): -c safe.directory names that one directory, nothing wider.
const pre = ['-C', dir, ...(safeDirectory ? ['-c', `safe.directory=${dir}`] : [])];
const pre = ['-C', dir, ...(safeDirectory ? ['-c', `safe.directory=${dir}`] : [])];
const run = async (args, { allowFail = false } = {}) => {
const run = async (args, { allowFail = false } = {}) => {
@@ -52,7 +52,7 @@ function httpsOrigin(value, where) {
@@ -52,7 +52,7 @@ function httpsOrigin(value, where) {
* untracked lockfiles the release tracks are removed first, preflight loads
* untracked lockfiles the release tracks are removed first, preflight loads
* static-build an in-place checkout with a build and repo vhosts and no process (Sites)
* static-build an in-place checkout with a build and repo vhosts and no process (Sites)
* pnpm-build an in-place pnpm workspace with a build (Games)
* pnpm-build an in-place pnpm workspace with a build (Games)
- * release-layout releases/<id> behind a `current` symlink (Live, OpenRe): a new release is prepared
+ * release-layout releases/<id> behind a `current` symlink (Live, OpenRestream): a new release is prepared
* while the old one serves, switching is an atomic rename, rollback switches back
* while the old one serves, switching is an atomic rename, rollback switches back
*/
*/
const STRATEGIES = ['git-checkout', 'multi-app', 'static-build', 'pnpm-build', 'release-layout'];
const STRATEGIES = ['git-checkout', 'multi-app', 'static-build', 'pnpm-build', 'release-layout'];
@@ -210,7 +210,7 @@ function normaliseService(id, raw, defaults) {
@@ -210,7 +210,7 @@ function normaliseService(id, raw, defaults) {
announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },
announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },
units: s.units || [],
units: s.units || [],
// Units that belong to the service but that ovhost never starts, stops or restarts: e.g.
// Units that belong to the service but that ovhost never starts, stops or restarts: e.g.
- // OpenRe's transport workers ([email protected], one instance per release), which
+ // OpenRestream's transport workers ([email protected], one instance per release), which
// drain on their own. status/validate list their instances read-only.
// drain on their own. status/validate list their instances read-only.
workerUnits: s.workerUnits || [],
workerUnits: s.workerUnits || [],
socketUnit: s.socketUnit || null,
socketUnit: s.socketUnit || null,
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
* Media: SELECT count(*) FROM vods WHERE is_recording = 1.
* Media: SELECT count(*) FROM vods WHERE is_recording = 1.
* postgresql-count a read-only SELECT against a database in the cluster, run as the postgres OS
* postgresql-count a read-only SELECT against a database in the cluster, run as the postgres OS
* user over peer auth (no secret is read, and a PostgreSQL service's database has
* user over peer auth (no secret is read, and a PostgreSQL service's database has
- * no file for a sqlite-count to open). OpenRe: ingest sessions in flight.
+ * no file for a sqlite-count to open). OpenRestream: ingest sessions in flight.
* sum the total of `probes` (each an http-json-count, sqlite-count or
* sum the total of `probes` (each an http-json-count, sqlite-count or
* postgresql-count): Tools' running
* postgresql-count): Tools' running
* jobs in each satellite's jobs.db. Unknown when any part is.
* jobs in each satellite's jobs.db. Unknown when any part is.
@@ -1,11 +1,11 @@
@@ -1,11 +1,11 @@
'use strict';
'use strict';
/**
/**
* The release-layout strategy (roadmap WS-N task 11): Live's deploy/scripts/deploy.sh (release layout)
* The release-layout strategy (roadmap WS-N task 11): Live's deploy/scripts/deploy.sh (release layout)
- * and OpenRe.Stream's deploy.sh `release` + `api`, as one engine.
+ * and OpenRestream's deploy.sh `release` + `api`, as one engine.
*
*
* <repo>/repo the git clone releases are made from (release.git), owned by `owner`
* <repo>/repo the git clone releases are made from (release.git), owned by `owner`
* <repo>/releases/<id> a detached worktree per release with its own node_modules
* <repo>/releases/<id> a detached worktree per release with its own node_modules
- * (id: <UTC yyyymmdd-HHMMSS>-<sha8> for Live, <sha12> for OpenRe)
+ * (id: <UTC yyyymmdd-HHMMSS>-<sha8> for Live, <sha12> for OpenRestream)
* <repo>/current -> releases/<id>, switched by an atomic rename (ln -s + mv -T)
* <repo>/current -> releases/<id>, switched by an atomic rename (ln -s + mv -T)
* release.links links made in every release (Live: data -> ../../shared/data)
* release.links links made in every release (Live: data -> ../../shared/data)
*
*
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
* 1. fetch; the target is origin/<branch> (or --to); nothing new and no --restart: done.
* 1. fetch; the target is origin/<branch> (or --to); nothing new and no --restart: done.
* 2. prepare the new release while the current one serves: worktree, links, `npm ci` when the
* 2. prepare the new release while the current one serves: worktree, links, `npm ci` when the
* lockfile or dependency fields changed (else the current node_modules hard-linked: cp -al), every
* lockfile or dependency fields changed (else the current node_modules hard-linked: cp -al), every
- * dependency resolves, preflight (Live: syntax check of the changed files), release.chown (OpenRe).
+ * dependency resolves, preflight (Live: syntax check of the changed files), release.chown (OpenRestream).
* Any failure removes the new release: the current one is untouched and nothing restarted (exit 2).
* Any failure removes the new release: the current one is untouched and nothing restarted (exit 2).
* 3. static-only change (everything outside noRestartPaths unchanged, unit files unchanged): switch
* 3. static-only change (everything outside noRestartPaths unchanged, unit files unchanged): switch
* `current`, wait release.settleSeconds, the ready URL must answer, else switch back (exit 3).
* `current`, wait release.settleSeconds, the ready URL must answer, else switch back (exit 3).
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
* new sha when ready.release). Not ready: switch back, put the previous release's unit files back,
* new sha when ready.release). Not ready: switch back, put the previous release's unit files back,
* restart, exit 3; if that is not ready either, exit 4 (MANUAL INTERVENTION).
* restart, exit 3; if that is not ready either, exit 4 (MANUAL INTERVENTION).
* 5. prune to release.keep, never the current release, the one just left, or one a worker unit
* 5. prune to release.keep, never the current release, the one just left, or one a worker unit
- * instance (OpenRe's openre-rtmp-ingest@<id>) still runs from.
+ * instance (OpenRestream's openre-rtmp-ingest@<id>) still runs from.
*
*
* The socket rule (Live): pid 1 must hold the socket unit's listener. When the socket unit file changed
* The socket rule (Live): pid 1 must hold the socket unit's listener. When the socket unit file changed
* in this deploy, or systemd does not hold the listener, the service is stopped, the socket restarted
* in this deploy, or systemd does not hold the listener, the service is stopped, the socket restarted
@@ -31,7 +31,7 @@
@@ -31,7 +31,7 @@
* only the service restarts and new connections queue on the socket. A listener still not systemd's
* only the service restarts and new connections queue on the socket. A listener still not systemd's
* after readiness is reported loudly and recorded (socketHeld: false).
* after readiness is reported loudly and recorded (socketHeld: false).
*
*
- * --prepare-only (OpenRe's `deploy.sh release`): step 2 and stop; a later `deploy --to <sha>` of the
+ * --prepare-only (OpenRestream's `deploy.sh release`): step 2 and stop; a later `deploy --to <sha>` of the
* same commit switches to it (sha12 ids name one release per commit, so it is reused).
* same commit switches to it (sha12 ids name one release per commit, so it is reused).
*
*
* rollback: back to the release the release log says the current one replaced (or --to <id|sha>, or
* rollback: back to the release the release log says the current one replaced (or --to <id|sha>, or
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* plan, deploy and rollback. A service's `strategy` (lib/inventory.js, docs/deploy-strategies.md)
* plan, deploy and rollback. A service's `strategy` (lib/inventory.js, docs/deploy-strategies.md)
- * picks the engine: release-layout services (Live, OpenRe) go to lib/release-layout.js; every other
+ * picks the engine: release-layout services (Live, OpenRestream) go to lib/release-layout.js; every other
* strategy (git-checkout, multi-app, static-build, pnpm-build) is an in-place git checkout driven
* strategy (git-checkout, multi-app, static-build, pnpm-build) is an in-place git checkout driven
* here, with the strategy's defaults. The rules this encodes come from the per-repository deploy
* here, with the strategy's defaults. The rules this encodes come from the per-repository deploy
* scripts it replaces:
* scripts it replaces:
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
# deploys (ovhost deploy), and makes the SQLite file read-only: it is the 7-day rollback (compatibility
# deploys (ovhost deploy), and makes the SQLite file read-only: it is the 7-day rollback (compatibility
# register C-89: sudo ovhost rollback <svc> to the release before, which reads the file).
# register C-89: sudo ovhost rollback <svc> to the release before, which reads the file).
#
#
-# A service that is not one unit in /opt/openvibe.<svc> (OpenRe: two units plus per-release transport workers, a
+# A service that is not one unit in /opt/openvibe.<svc> (OpenRestream: two units plus per-release transport workers, a
# release layout) sets, in the environment:
# release layout) sets, in the environment:
# SWITCH_UNITS="a.service b.service 'worker@*.service'" every unit to stop (the first one's environment runs the
# SWITCH_UNITS="a.service b.service 'worker@*.service'" every unit to stop (the first one's environment runs the
# import); a pattern stops every running instance
# import); a pattern stops every running instance
@@ -22,7 +22,7 @@ const KEY_HEX = crypto.createHash('sha256').update('test backup key').digest('he
@@ -22,7 +22,7 @@ const KEY_HEX = crypto.createHash('sha256').update('test backup key').digest('he
const S3_SECRET = `${SECRET}-s3`;
const S3_SECRET = `${SECRET}-s3`;
const TENANT_BLOB = 'tenant object bytes\n';
const TENANT_BLOB = 'tenant object bytes\n';
-/** The standard fake host plus AI (two databases) and OpenRe (not deployed), a backup env and key. */
+/** The standard fake host plus AI (two databases) and OpenRestream (not deployed), a backup env and key. */
function backupHost({ envMode = 0o600, keyMode = 0o600, envOwner = 'root', env = null } = {}) {
function backupHost({ envMode = 0o600, keyMode = 0o600, envOwner = 'root', env = null } = {}) {
const host = scenario();
const host = scenario();
const doc = JSON.parse(host.read('/etc/openvibe/host.json'));
const doc = JSON.parse(host.read('/etc/openvibe/host.json'));
@@ -26,7 +26,7 @@ runTests([
@@ -26,7 +26,7 @@ runTests([
// Every service on the host is deployed by a strategy (WS-N task 11): Games by pnpm-build.
// Every service on the host is deployed by a strategy (WS-N task 11): Games by pnpm-build.
assert.ok(Object.values(inv.services).every((s) => s.managed), 'every service is managed');
assert.ok(Object.values(inv.services).every((s) => s.managed), 'every service is managed');
assert.deepStrictEqual(Object.fromEntries(['live', 'tools', 'sites', 'games', 'openre', 'network'].map((id) => [id, inv.services[id].strategy])), { live: 'release-layout', tools: 'multi-app', sites: 'static-build', games: 'pnpm-build', openre: 'release-layout', network: 'git-checkout' });
assert.deepStrictEqual(Object.fromEntries(['live', 'tools', 'sites', 'games', 'openre', 'network'].map((id) => [id, inv.services[id].strategy])), { live: 'release-layout', tools: 'multi-app', sites: 'static-build', games: 'pnpm-build', openre: 'release-layout', network: 'git-checkout' });
- // Live's and OpenRe's release layouts are root-owned clones (git as root); their services and drills run as ubuntu.
+ // Live's and OpenRestream's release layouts are root-owned clones (git as root); their services and drills run as ubuntu.
for (const s of Object.values(inv.services)) assert.strictEqual(s.owner, ['live', 'openre'].includes(s.id) ? 'root' : 'ubuntu', `${s.id} checkout owner`);
for (const s of Object.values(inv.services)) assert.strictEqual(s.owner, ['live', 'openre'].includes(s.id) ? 'root' : 'ubuntu', `${s.id} checkout owner`);
assert.strictEqual(inv.services.live.runAs, 'ubuntu');
assert.strictEqual(inv.services.live.runAs, 'ubuntu');
assert.strictEqual(inv.services.openre.runAs, 'ubuntu');
assert.strictEqual(inv.services.openre.runAs, 'ubuntu');
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
'use strict';
'use strict';
/**
/**
- * OpenRe on ovhost: `workerUnits` (the transport worker templates) are listed by status and
+ * OpenRestream on ovhost: `workerUnits` (the transport worker templates) are listed by status and
* validate and never started, stopped or restarted; the inventory refuses a worker unit in
* validate and never started, stopped or restarted; the inventory refuses a worker unit in
* `units`; a deploy of a git-layout service with worker units restarts only its units; and the
* `units`; a deploy of a git-layout service with worker units restarts only its units; and the
* example inventory's openre drill starts openre-api alone with OPENRE_DRILL=1 and the side-effect
* example inventory's openre drill starts openre-api alone with OPENRE_DRILL=1 and the side-effect
@@ -22,7 +22,7 @@ const WORKERS = [
@@ -22,7 +22,7 @@ const WORKERS = [
`openre-webrtc@${RELEASE}.service`,
`openre-webrtc@${RELEASE}.service`,
];
];
-/** The standard fake host plus OpenRe as on openvibe-ovh: release layout, two units, worker instances. */
+/** The standard fake host plus OpenRestream as on openvibe-ovh: release layout, two units, worker instances. */
async function openreHost({ workersRunning = true } = {}) {
async function openreHost({ workersRunning = true } = {}) {
const host = scenario();
const host = scenario();
const doc = JSON.parse(host.read('/etc/openvibe/host.json'));
const doc = JSON.parse(host.read('/etc/openvibe/host.json'));
@@ -91,7 +91,7 @@ async function openreHost({ workersRunning = true } = {}) {
@@ -91,7 +91,7 @@ async function openreHost({ workersRunning = true } = {}) {
const stateChanges = (host) => host.calls.filter((c) => c.cmd === 'systemctl' && !['show', 'is-active', 'is-enabled', 'cat', 'status', 'list-units'].includes(c.args[0]));
const stateChanges = (host) => host.calls.filter((c) => c.cmd === 'systemctl' && !['show', 'is-active', 'is-enabled', 'cat', 'status', 'list-units'].includes(c.args[0]));
runTests([
runTests([
- test('the example inventory declares the OpenRe worker templates and a supported drill', () => {
+ test('the example inventory declares the OpenRestream worker templates and a supported drill', () => {
const inv = normalise(EXAMPLE);
const inv = normalise(EXAMPLE);
const o = inv.services.openre;
const o = inv.services.openre;
assert.deepStrictEqual(o.units, ['openre-api.service', 'openre-session-coordinator.service']);
assert.deepStrictEqual(o.units, ['openre-api.service', 'openre-session-coordinator.service']);
@@ -151,7 +151,7 @@ async function liveHost(overrides = {}) {
@@ -151,7 +151,7 @@ async function liveHost(overrides = {}) {
return host;
return host;
}
}
-// ── OpenRe: release layout, sha12 ids, workers ───────────────────────────────
+// ── OpenRestream: release layout, sha12 ids, workers ───────────────────────────────
function openreEntry(overrides = {}) {
function openreEntry(overrides = {}) {
return {
return {
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * The release-layout strategy (lib/release-layout.js) against the fake host, as Live and OpenRe run on
- * the real one: every rule of Live's deploy/scripts/deploy.sh (release layout) and OpenRe's
+ * The release-layout strategy (lib/release-layout.js) against the fake host, as Live and OpenRestream run on
+ * the real one: every rule of Live's deploy/scripts/deploy.sh (release layout) and OpenRestream's
* deploy.sh release + api, driven through `ovhost plan|deploy|rollback`.
* deploy.sh release + api, driven through `ovhost plan|deploy|rollback`.
*/
*/
const assert = require('assert');
const assert = require('assert');
@@ -318,7 +318,7 @@ runTests([
@@ -318,7 +318,7 @@ runTests([
assert.strictEqual(legacy.services.x.managed, false, 'a release layout without the strategy stays with its own script');
assert.strictEqual(legacy.services.x.managed, false, 'a release layout without the strategy stays with its own script');
}),
}),
- // ── OpenRe ──
+ // ── OpenRestream ──
test('openre: release <sha12> with npm ci and chown to ubuntu, API and coordinator restarted, workers never touched', async () => {
test('openre: release <sha12> with npm ci and chown to ubuntu, API and coordinator restarted, workers never touched', async () => {
const host = await openreHost();
const host = await openreHost();
const first = host.firstRelease;
const first = host.firstRelease;
@@ -327,7 +327,7 @@ runTests([
@@ -327,7 +327,7 @@ runTests([
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(host.current(), to.slice(0, 12));
assert.strictEqual(host.current(), to.slice(0, 12));
const npm = host.calls.filter((c) => c.cmd === 'npm');
const npm = host.calls.filter((c) => c.cmd === 'npm');
- assert.strictEqual(npm.length, 1, 'OpenRe installs into every release (reuseModules false)');
+ assert.strictEqual(npm.length, 1, 'OpenRestream installs into every release (reuseModules false)');
assert.strictEqual(npm[0].cwd, `/opt/openre.stream/releases/${to.slice(0, 12)}`);
assert.strictEqual(npm[0].cwd, `/opt/openre.stream/releases/${to.slice(0, 12)}`);
assert.ok(host.calls.some((c) => c.cmd === 'chown' && c.args.join(' ') === `-R ubuntu:ubuntu /opt/openre.stream/releases/${to.slice(0, 12)}`));
assert.ok(host.calls.some((c) => c.cmd === 'chown' && c.args.join(' ') === `-R ubuntu:ubuntu /opt/openre.stream/releases/${to.slice(0, 12)}`));
assert.strictEqual(host.files.get(`/opt/openre.stream/releases/${to.slice(0, 12)}/server/index.js`).owner, 'ubuntu');
assert.strictEqual(host.files.get(`/opt/openre.stream/releases/${to.slice(0, 12)}/server/index.js`).owner, 'ubuntu');