openvibers/openvibe.sites Commits

Freeze product pages and build only domain notices (#7)

OP OpenVibers committed 4 Oct 2026 · fa21c63195 · 52 files, +1310 −995

local checks ✓ 3/3 · CI ✓ 3/3 · review SHIP by DeepSeek harness (QUALITY=high) · 1 fix round

https://github.com/OpenVibers/OpenVibe.Sites/pull/7

Changes

ddiff --git a/README.md b/README.md
iindex 674f6b3..29390ac 100644
---- a/README.md
++++ b/README.md
@@@ -1,112 +1,32 @@
# OpenVibe.Sites
--## Purpose
++OpenVibe.Sites serves static notices for three addresses during the domain handoff. It no longer builds product placeholders. `notices.json` is the build input; `frozen.json` records the 32 product domains whose existing `dist/<domain>/index.html` and nginx vhosts remain in place until their owners complete the DNS and nginx cutover. OpenVibe.Bot owns its own vhost.
--Static front pages for the OpenVibe domains that are not public applications yet. As of
--2026-09-24 (`sites.json`; production ran `18166b1` when this was written): `openvibe.news`,
--`.reviews`, `.tips`, `.vip`, `.trade`, `.host`, `.deals`, `.coupons`, `openre.stream`, the planned products added
--2026-09-28 whose repositories do not exist yet (`noRepo`: `openvibe.pics`, `.download`, `.space`, `.food`, `.quest`,
--`.rent`, `.homes`, `.services`, `.run`, `.video`, `.website`, `.help`, `.bot`, `.watch`, `.actor`), `openvibe.events`
--(OpenVibe.Events' future home; its API runs at events.openvibe.network), `ai.openvibe.services` (OpenVibe.AI's
--public home), and `auth`, `api`, `admin` and `themes` under `openvibe.network`. Most of these already run on the host
--behind loopback (News, Reviews, Tips, VIP, Trade, Host, Deals, Coupons, OpenRe.Stream, AI); the page stays until the service launches publicly on its domain, and says
--so (see facts below). Three more are notices, not placeholders (`kind` in `sites.json`: noindex, no
--sitemap, not listed as opening): `ai.openvibe.network` says OpenVibe.AI moved to `ai.openvibe.services` (it redirects once
--every client has moved), `realtime.openvibe.network` says OpenVibe.Realtime is closed (ADR-005:
--realtime delivery is part of OpenVibe.Events), and `status.openvibe.network` points at the status
--Network publishes (`openvibe.network/status`), with a live summary read in the browser from the
--CORS-open `/api/v1/registry/health`.
--`openvibe.codes`, `openvibe.wiki`, `openvibe.blog`, `openvibe.chat` (2026-09-24), `events.openvibe.network` and
--`billing.openvibe.network` have left for their own services. One catalog (`sites.json`), one template
--(`build.js`), real pages: what the site will be, what to use on the network meanwhile, the whole
--network, sign-in — with the OpenVibe Frame (navbar, footer, theme loader) from this repository's
--pinned openvibe-shared, copied into `dist/_shared/` and served at `/shared/` on every placeholder, so a
--page keeps its frame while Network is down; full SEO (canonical, Open Graph, Twitter, JSON-LD, robots,
--sitemap, manifest).
++## Remaining notices
--## Owns
++| Address | Purpose | Vhost owner |
++| --- | --- | --- |
++| `ai.openvibe.network` | Moved address for OpenVibe.AI | Sites |
++| `realtime.openvibe.network` | Closed service notice | Sites |
++| `status.openvibe.network` | Pointer to Network status | Sites |
--- the catalog `sites.json`, the template `build.js` and the generated `dist/<domain>/` pages
-- (checked in), with a `404.html` and a `release.json` per domain
--- the nginx vhosts `deploy/nginx/<domain>.conf` for those domains
--- `facts.json`, the committed snapshot of each repository's `STATUS.json` and Network's registry exposure
++All three are `noindex`. The existing page presentation is held in `notice-pages/`; the notice build writes only these domains and shared browser assets. The status notice still reads Network's public health endpoint in the browser.
--## Does not own
++## Frozen product pages
--- any product: a domain leaves the catalog when its own service launches on it
--- the facts themselves (each repository's `STATUS.json`, Network's registry), the Frame (OpenVibe.Shared)
--- certificates (certbot on the host) and DNS (Cloudflare)
++`frozen.json` lists all 32 product domains, including OpenVibe.Actor and OpenVibe.Bot. Their committed front pages remain available as static files with their original indexing, status, and sitemaps. The build does not regenerate them. `node build.js --check` verifies that each frozen domain still has an index page and a Sites vhost where Sites owns one, while checking that notice outputs are current. The old catalog and repository facts snapshots are historical data, not build inputs.
--## Depends on
++The owner cutover will transfer DNS and nginx ownership one domain at a time after each product can serve its address. That work is outside this repository change. Until then, the existing product pages and vhosts continue to serve their addresses. A missing file path is a real 404 through `try_files ... =404` and `error_page 404 /404.html`.
--- `openvibe-shared` v2.5.0 (the Frame files, SEO helpers), pinned by release tarball
--- the sibling checkouts' `STATUS.json` and Network's `/api/v1/registry/services` (only when refreshing
-- `facts.json`); in the browser, the status page reads Network's CORS-open `/api/v1/registry/health`
--- OpenVibe.Host (`ovhost deploy sites`, strategy `static-build`) and nginx on the host
--
--## Capabilities
--
--None: Sites implements no capability, holds no grant and calls no service with a token. The pages load
--only their own files, and the status notice reads Network's public health endpoint in the browser.
--
--## Tests and build
++## Build and verification
++```sh
++node build.js
++node build.js --check
++ov test test/build.test.js
++ov test test/deploy-wrapper.test.js
```
--npm ci # openvibe-shared: the pinned OpenVibe.Shared release (package.json)
--node scripts/facts.js # refresh facts.json (each repo's STATUS.json + Network's registry exposure)
--node build.js # dist/<domain>/… + deploy/nginx/<domain>.conf
--node build.js --check # fails when dist/ is stale
--npm test # dist current; labels match facts.json; facts.json matches the sibling STATUS.json files
--```
--
--What a page says about its product comes from `facts.json`, never from a hand-kept list: each
--repository's committed `STATUS.json` (read from the sibling `~/OpenVibers/<repo>` checkouts at HEAD)
--and Network's registry exposure (`/api/v1/registry/services`). A product whose service runs is
--labelled "in development" with what its public launch waits for (`launch` in `sites.json`); a
--subdomain of a live service says it is not routed yet; only a repository with no code says
--"charter only". The build reads the committed snapshot, so it is reproducible; `npm test` fails
--locally when a sibling's `STATUS.json` stage or code flag no longer matches it.
--
--`npm test` runs `test/run.js`, which runs each `test/*.test.js` in its own process: `test/build.test.js`
--(dist is current; every label matches `facts.json`; no shared file comes from openvibe.network and each
--`/shared` URL carries its file's hash; a live service's domain is never also a placeholder; 404 pages)
--and `test/deploy-wrapper.test.js` (the deploy wrapper and its fallback, against a fake ovhost and a temp
--checkout).
--
--Every domain also gets a `404.html`: its vhost answers any path that is not a file in
--`dist/<domain>/` with status 404 and that page (`try_files … =404` + `error_page 404 /404.html`),
--never the front page.
--
--## Security
--
--Reporting a vulnerability: [SECURITY.md](SECURITY.md). The pages are static: no server code, no
--forms and no secrets (the Frame's navbar asks Network for the visitor's session, as on every site). Every vhost sets `X-Content-Type-Options`,
--`X-Frame-Options`, `Referrer-Policy` and HSTS (repeated in each location that adds headers), and answers
--unknown paths with 404. Placeholders are `noindex` where they are notices and never claim a product is
--live (`test/build.test.js` holds labels to `facts.json`).
--
--## Deploy
--
--Deploy (host): the repo lives at `/opt/openvibe.sites`. `sudo deploy/scripts/deploy.sh` runs
--`ovhost deploy sites --restart` (OpenVibe.Host, strategy `static-build`; `DRY_RUN=1` for the plan). ovhost
--pulls first, as the checkout owner, so a stale checkout never installs old vhosts: it restores the tracked
--`dist/` the last build rewrote, fast-forwards, runs `npm ci` and `node build.js`, installs and enables every
--`deploy/nginx/*.conf` behind `nginx -t` (a failing test puts the previous vhost files back, never reloads
--nginx, and restores the checkout and `dist/`), then reloads nginx. Each domain has a Let's Encrypt wildcard
--certificate (`certbot --dns-cloudflare`, see the host's renewal configs). Then, for each placeholder, it
--publishes `host.release.published` with the service and release id from that placeholder's `release.json`
--and `https://<domain>` as the origin, once per release, so open tabs check `/release.json` within seconds
--(WS-P task 9); this is best effort, stops at the first failure and never fails the deploy. A vhost that left
--`deploy/nginx/` stays installed and is named in the output: remove it by hand once its domain is served
--elsewhere. openvibe.bot is the exception to removing it by hand: sites.json gives its vhost to OpenVibe.Bot
--(`vhostOwner`), whose first deploy installs its own openvibe.bot.conf over the one left here; until then the
--Sites vhost keeps serving the page. When ovhost is missing, too old or does not deploy Sites with that
--strategy, the wrapper pulls and runs `deploy/scripts/deploy-legacy.sh`, the previous script, unchanged
--(`OVHOST_LEGACY=1` forces it).
--Rollback: there is no ready URL, so nothing is automatic; `sudo ovhost rollback sites --to <sha>` rebuilds
--that commit and installs its vhosts behind `nginx -t`. There is no env file, unit or port: nginx serves
--`dist/` directly.
++The pinned `openvibe-shared` release supplies the Frame browser files under `dist/_shared/`. The deploy wrapper remains the existing OpenVibe.Host static build path. Sites has no server process, port, token, or environment file. The owner manages certificates and DNS.
--When a domain becomes a real app, delete it from `sites.json`, remove its vhost, and point the
--domain's nginx block at the new service — nothing else references it.
++Report security issues through [SECURITY.md](SECURITY.md).
ddiff --git a/STATUS.json b/STATUS.json
iindex 2c1d0cc..8a5fcf3 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -42,12 +42,12 @@
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy sites --restart` (OpenVibe.Host strategy static-build since the 2026-09-27 cutover): /opt/openvibe.sites, nginx serves dist/<domain>/ through deploy/nginx/<domain>.conf; no unit, no port, no env file",
"features": [
-- "one catalog and one template for every placeholder and notice page",
-- "labels built from facts.json (each repository's STATUS.json and Network's registry), never a hand-kept list",
++ "three generated notices and 32 frozen product domains",
++ "notice pages use preserved presentation; product pages are no longer generated",
"the pinned OpenVibe Frame served from dist/_shared/",
-- "full SEO: canonical, Open Graph, Twitter, JSON-LD, robots, sitemap, manifest",
-- "404 pages and generated nginx vhosts",
++ "noindex notices with robots, manifest and real 404 pages",
++ "generated notice vhosts and preserved product vhosts",
"a release.json per domain and host.release.published on deploy"
],
-- "updated": "2026-09-28"
++ "updated": "2026-10-03"
}
ddiff --git a/build.js b/build.js
iindex 1de7773..d393b10 100644
---- a/build.js
++++ b/build.js
@@@ -1,479 +1,23 @@
#!/usr/bin/env node
'use strict';
--/**
-- * OpenVibe.Sites — static front pages for the network's domains that are not full apps yet.
-- *
-- * node build.js → dist/<domain>/{index.html,robots.txt,sitemap.xml,manifest.webmanifest,status.json,release.json}
-- * node build.js --check → exit 1 when dist/ is stale (CI / deploy guard)
-- *
-- * One template, one catalog (sites.json), one snapshot of facts (facts.json, refreshed by
-- * scripts/facts.js from each repository's STATUS.json and the Network registry). Every page is real
-- * content for people and crawlers: what the site will be, where its product stands today (from
-- * facts.json, never a hand-kept list), what to use meanwhile, the whole network, sign-in — with the
-- * shared navbar/footer/theme loader from openvibe.network so the domain already feels like the rest
-- * of the network. No build step at request time: nginx serves the files.
-- */
++/** OpenVibe.Sites: rebuild notices only; product pages and vhosts are frozen. */
const fs = require('fs');
const path = require('path');
--// The network's legal documents, from the pinned OpenVibe.Shared release (package.json).
++const crypto = require('crypto');
const legal = require('openvibe-shared/legal');
--// D42 (roadmap WS-P task 4): the pages run this repository's pinned copy of the shared browser files, served by
--// every Sites vhost at /shared/ from one copy in dist/_shared/, content-addressed (?v=), never openvibe.network's.
const sharedFiles = require('openvibe-shared/files');
--const SHARED = (name) => require('openvibe-shared/serve').url(name);
--// Which clauses apply to each domain once it opens (mirrors OpenVibe.Network/server/frame/sites.js).
--const LEGAL_PROFILE = { chat: 'ugc', codes: 'ugc', blog: 'info', wiki: 'ugc', news: 'info', reviews: 'ugc', tips: 'streaming', vip: 'account', trade: 'ugc', host: 'hosting', deals: 'info', coupons: 'info', stream: 'streaming' };
--// Sites whose own server has no page routes: their legal pages are built here and served by nginx.
--const LEGAL_ONLY = [{ domain: 'openvibe.games', name: 'OpenVibe.Games', id: 'games', profile: 'games' }];
--const legalSite = (site) => ({ id: site.tld, service: 'network', host: site.domain, name: site.name, profile: site.legalProfile || LEGAL_PROFILE[site.tld] || 'info' });
--
++const notices = require('./notices.json');
++const frozen = require('./frozen.json');
const ROOT = __dirname;
const DIST = path.join(ROOT, 'dist');
--const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, 'sites.json'), 'utf8'));
--// What is true about each repository a page names: its committed STATUS.json and Network's registry
--// exposure, snapshotted by scripts/facts.js. The build reads only this file, so it is reproducible.
--const facts = JSON.parse(fs.readFileSync(path.join(ROOT, 'facts.json'), 'utf8'));
--const NET = catalog.network;
--const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
--
--/** The facts row for a site's repository (null when the page names none). A missing row fails the build. */
--function repoFacts(site) {
-- if (!site.plannedRepo) return null;
-- const f = facts.repos[site.plannedRepo];
-- if (!f) throw new Error(`${site.domain}: facts.json has no row for ${site.plannedRepo}; run node scripts/facts.js`);
-- return f;
--}
--/**
-- * Where the product behind a page stands, from facts only:
-- * closed the repository's STATUS.json says closed (a decision record, nothing to launch)
-- * surface the repository's service is public elsewhere; this domain is not routed to it yet
-- * running the service is built and runs on the host (loopback only); the domain is not public yet
-- * code code exists, nothing deployed
-- * planned charter only, no code
-- * pointer (kind: status) the address points at something a live service publishes elsewhere
-- */
--function standing(site) {
-- if (site.kind === 'status') return 'pointer';
-- // A notice about a closed product says so even when its repository is archived and named nowhere.
-- if (site.kind === 'closed') return 'closed';
-- const f = repoFacts(site);
-- if (!f) return 'planned';
-- const st = f.status || {}; const rg = f.registry || {};
-- if (st.stage === 'closed' || rg.state === 'closed') return 'closed';
-- if (rg.state === 'live' && rg.public_site === 'service') return 'surface';
-- if (rg.state === 'internal' || (st.code && st.deployed)) return 'running';
-- if (st.code) return 'code';
-- return 'planned';
--}
--const STANDING = {
-- running: { label: 'in development', repoStage: 'service-running' },
-- code: { label: 'in development', repoStage: 'code-not-deployed' },
-- surface: { label: 'not routed yet', repoStage: 'surface-of-a-live-service' },
-- planned: { label: 'planned', repoStage: 'charter-only' },
-- closed: { label: 'closed', repoStage: 'closed' },
-- pointer: { label: 'status', repoStage: 'pointer' },
--};
--// The stable marker other repositories' checks grep for (Host docs/launch.md, Network's requirement
--// ledger): every placeholder says it, a notice never does.
--const MARKER = 'this page is a placeholder, nothing here is live yet';
--const repoLink = (repo) => `<a href="https://github.com/OpenVibers/${esc(repo)}" rel="noopener"><code>OpenVibers/${esc(repo)}</code></a>`;
--/** The hero's status sentence: what is true about the product, and what its public launch waits for. */
--function statusText(site) {
-- const k = standing(site);
-- const f = repoFacts(site);
-- // A product planned before its repository exists names itself, never another repository.
-- const repo = site.plannedRepo ? repoLink(site.plannedRepo) : site.noRepo ? `<strong>${esc(site.name)}</strong>` : '<code>OpenVibe.Network</code>';
-- const waits = site.launch ? ` The public launch is waiting for ${esc(site.launch)}.` : '';
-- // A product that moved to another address (site.kind: moved): the first link is where it went.
-- if (site.kind === 'moved') return `${repo} moved to <a href="${esc(site.links[0][1])}">${esc(site.links[0][1].replace(/^https?:\/\/|\/$/g, ''))}</a>; this address serves only this notice.`;
-- if (k === 'running') return `${repo} is built and its service runs on the network's host, but it is not public yet: ${MARKER}.${waits}`;
-- if (k === 'code') return `${repo} has code, but nothing is deployed yet: ${MARKER}.${waits}`;
-- if (k === 'surface') {
-- const o = f.registry.origin;
-- return `${repo} is live at <a href="${esc(o)}/">${esc(o.replace(/^https?:\/\//, ''))}</a>; this address is not routed to it yet: ${MARKER}.`;
-- }
-- if (k === 'pointer') return `${repo} publishes the status of every OpenVibe service at <a href="${esc(site.links[0][1])}">${esc(site.links[0][1].replace(/^https?:\/\//, ''))}</a>; this address points there.`;
-- if (k === 'closed') return `${repo} is closed and stays as a decision record; no product launches at this address.`;
-- if (site.noRepo) return `${repo} is planned; its repository and code do not exist yet: ${MARKER}.${waits}`;
-- return `${repo}: charter only, no code yet: ${MARKER}.${waits}`;
--}
++const NET = { networkUrl: 'https://openvibe.network' };
const today = new Date().toISOString().slice(0, 10);
--// A page is first published once; rebuilding it later changes dateModified, never datePublished.
--const FIRST_PUBLISHED = '2026-09-17';
--
--function hexToRgb(hex) { const m = String(hex).replace('#', ''); const n = parseInt(m.length === 3 ? m.split('').map(c => c + c).join('') : m, 16); return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`; }
--
--/** A site's headline feature, shown right under the hero: { title, text, points: [..], cta: [label, href], more: [label, href] }. */
--function highlight(h) {
-- return ` <section class="highlight" aria-labelledby="highlight-h">
-- <div class="hl-card">
-- <h2 id="highlight-h"><i class="fa-solid fa-bolt" aria-hidden="true"></i> ${esc(h.title)}</h2>
-- <p>${esc(h.text)}</p>
-- ${(h.points || []).length ? `<ul>${h.points.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : ''}
-- <div class="ctas">${h.cta ? `<a class="btn btn-primary" href="${esc(h.cta[1])}"><i class="fa-solid fa-tower-broadcast"></i> ${esc(h.cta[0])}</a>` : ''}${h.more ? ` <a class="btn" href="${esc(h.more[1])}">${esc(h.more[0])}</a>` : ''}</div>
-- </div>
-- </section>`;
--}
--
--// A product with a worked-out vision (site.vision, owner 2026-09-29: Bot and Actor) gets a front door instead of the plain
--// "what it will be" grid: a split hero with a labelled design preview, how it works, the choices, what it does, how it
--// connects to the network, and its questions. It stays honest: the build status (with MARKER) says nothing is live yet.
--function visionViz(v) {
-- const z = v.viz || {};
-- if (z.kind === 'panel') {
-- return `<figure class="viz" aria-label="${esc(z.label)}">
-- <div class="viz-top"><span class="dot"></span><b>${esc(z.name)}</b><span class="muted">online</span><span class="lat"><i class="fa-solid fa-signal" aria-hidden="true"></i> ${esc(z.latency)}</span></div>
-- <div class="viz-video" aria-hidden="true"><div class="scan"></div><span class="onair"><i class="fa-solid fa-circle" aria-hidden="true"></i> ${esc(z.onair)}</span><span class="cam"><i class="fa-solid fa-video" aria-hidden="true"></i> Front camera</span></div>
-- <div class="viz-ctrl" aria-hidden="true">
-- <div class="dpad"><span class="u"><i class="fa-solid fa-caret-up"></i></span><span class="l"><i class="fa-solid fa-caret-left"></i></span><span class="c"></span><span class="r"><i class="fa-solid fa-caret-right"></i></span><span class="d"><i class="fa-solid fa-caret-down"></i></span></div>
-- <div class="viz-side">
-- <div class="keys">${(z.keys || []).map((k) => `<span><i class="fa-solid ${esc(k[0])}"></i> ${esc(k[1])}</span>`).join('')}</div>
-- <div class="meter"><span>Battery</span><i style="--v:${Number(z.battery) || 0}%"></i><b>${Number(z.battery) || 0}%</b></div>
-- <div class="estop">STOP</div>
-- </div>
-- </div>
-- <figcaption>${esc(z.caption)}</figcaption>
-- </figure>`;
-- }
-- if (z.kind === 'agent') {
-- return `<figure class="viz" aria-label="${esc(z.label)}">
-- <div class="viz-top"><span class="av"><i class="fa-solid ${esc(z.icon)}" aria-hidden="true"></i></span><b>${esc(z.name)}</b><span class="muted">${esc(z.role)}</span><span class="lat"><i class="fa-solid fa-cloud" aria-hidden="true"></i> ${esc(z.state)}</span></div>
-- <ol class="feed">${(z.steps || []).map((t) => `<li><i class="fa-solid fa-circle-check" aria-hidden="true"></i><span>${esc(t)}</span></li>`).join('')}
-- <li class="ask"><i class="fa-solid fa-hand" aria-hidden="true"></i><span>${esc(z.ask)}</span><span class="pills" aria-hidden="true"><span class="ok">Approve</span><span>Not yet</span></span></li></ol>
-- <figcaption>${esc(z.caption)}</figcaption>
-- </figure>`;
-- }
-- if (z.kind === 'router') {
-- return `<figure class="viz" aria-label="${esc(z.label)}">
-- <div class="viz-top"><span class="av"><i class="fa-solid fa-route" aria-hidden="true"></i></span><b>New task</b><span class="lat"><i class="fa-solid fa-wallet" aria-hidden="true"></i> ${esc(z.budget)}</span></div>
-- <p class="task">${esc(z.task)}</p>
-- <div class="modes" aria-hidden="true">${(z.modes || []).map((m) => `<span${m === z.mode ? ' class="on"' : ''}>${esc(m)}</span>`).join('')}</div>
-- <ol class="cands" aria-hidden="true">${(z.candidates || []).map((c) => `<li${c.pick ? ' class="pick"' : ''}><i class="fa-solid ${esc(c.icon)}"></i><b>${esc(c.name)}</b><span>${esc(c.cost)}</span><span>${esc(c.time)}</span><span>${esc(c.success)}</span>${c.pick ? '<em>picked</em>' : ''}</li>`).join('')}</ol>
-- <p class="why"><i class="fa-solid fa-circle-info" aria-hidden="true"></i> ${esc(z.why)}</p>
-- <figcaption>${esc(z.caption)}</figcaption>
-- </figure>`;
-- }
-- return '';
--}
--function vision(site) {
-- const v = site.vision;
-- const cards = (list) => list.map(([ic, h, d, href]) => href
-- ? `<a class="card" href="${esc(href)}"><div class="ic"><i class="fa-solid ${esc(ic)}" aria-hidden="true"></i></div><b>${esc(h)}</b><span>${esc(d)}</span><span class="go">${esc(href.replace(/^https?:\/\//, '').replace(/\/$/, ''))} →</span></a>`
-- : `<div class="card"><div class="ic"><i class="fa-solid ${esc(ic)}" aria-hidden="true"></i></div><b>${esc(h)}</b><span>${esc(d)}</span></div>`).join('\n ');
-- return ` <section id="what">
-- <h2>${esc(v.stepsTitle)}</h2>
-- <p class="lead2">${esc(v.stepsLead)}</p>
-- <ol class="steps">
-- ${v.steps.map(([h, d], i) => `<li><span class="n">${i + 1}</span><b>${esc(h)}</b><span>${esc(d)}</span></li>`).join('\n ')}
-- </ol>
-- </section>
--${v.choices ? `
-- <section>
-- <h2>${esc(v.choices.title)}</h2>
-- <p class="lead2">${esc(v.choices.lead)}</p>
-- <div class="choice">
-- ${v.choices.items.map((c) => `<div class="card big"><div class="ic"><i class="fa-solid ${esc(c.icon)}" aria-hidden="true"></i></div><b>${esc(c.title)}</b><span>${esc(c.text)}</span>${c.chips ? `<div class="chips">${c.chips.map((x) => `<span>${esc(x)}</span>`).join('')}</div>` : ''}</div>`).join('\n ')}
-- </div>
-- </section>` : ''}
--
-- <section>
-- <h2>${esc(v.featuresTitle)}</h2>
-- <p class="lead2">${esc(v.featuresLead)}</p>
-- <div class="grid three">
-- ${cards(v.features)}
-- </div>
-- </section>
--${(v.more || []).map((m) => `
-- <section${m.id ? ` id="${esc(m.id)}"` : ''}>
-- <h2>${esc(m.title)}</h2>
-- <p class="lead2">${esc(m.lead)}</p>
-- <div class="grid three">
-- ${cards(m.cards)}
-- </div>
-- </section>`).join('')}
--${v.templates ? `
-- <section>
-- <h2>${esc(v.templates.title)}</h2>
-- <p class="lead2">${esc(v.templates.lead)}</p>
-- <div class="chips big">${v.templates.items.map(([ic, t]) => `<span><i class="fa-solid ${esc(ic)}" aria-hidden="true"></i>${esc(t)}</span>`).join('')}</div>
-- </section>` : ''}
--
-- <section class="meanwhile">
-- <h2>${esc(v.connectTitle)}</h2>
-- <p class="lead2">${esc(v.connectLead)}</p>
-- <div class="grid">
-- ${cards(v.connect)}
-- </div>
-- </section>
--${site.faq && site.faq.length ? `
-- <section id="questions" class="faq">
-- <h2>Questions</h2>
-- ${site.faq.map(([q, ans]) => `<details><summary>${esc(q)}</summary><p>${esc(ans)}</p></details>`).join('\n ')}
-- </section>` : ''}
--
-- <section class="build">
-- <div class="card">
-- <b><i class="fa-solid fa-person-digging" aria-hidden="true"></i> Build status</b>
-- <span>${esc(v.building)} Its own service is not running yet: ${MARKER}.${site.launch ? ` It opens with ${esc(site.launch)}.` : ''} · as of ${esc(facts.generated)} · <a href="/status.json">status.json</a></span>
-- </div>
-- </section>
--`;
--}
--
--function page(site) {
-- const core = site.core || NET.core;
-- const url = `https://${site.domain}/`;
-- const title = `${site.name} — ${site.tagline}`;
-- const ogImage = `${NET.networkUrl}/assets/logo-512.png`;
-- const others = NET.liveSites.filter(s => s.url !== url.slice(0, -1));
-- // A notice (site.kind: closed or moved) is not a placeholder for a product: it says where the work went, is not
-- // indexed and is not listed among the domains still to open.
-- const notice = !!site.kind;
-- const siblings = catalog.sites.filter(s => s.domain !== site.domain && !s.kind);
-- const primary = notice && site.links && site.links[0];
-- const ld = {
-- '@context': 'https://schema.org',
-- '@graph': [
-- { '@type': 'WebSite', '@id': `${url}#site`, url, name: site.name, description: site.description, inLanguage: 'en',
-- publisher: { '@type': 'Organization', '@id': `${NET.networkUrl}/#org`, name: 'OpenVibe', url: `${NET.networkUrl}/`, logo: ogImage, sameAs: [NET.github, NET.discord] },
-- isPartOf: { '@type': 'WebSite', '@id': `${NET.networkUrl}/#site`, name: 'OpenVibe.Network', url: `${NET.networkUrl}/` } },
-- { '@type': 'WebPage', '@id': `${url}#page`, url, name: title, description: site.description, isPartOf: { '@id': `${url}#site` }, datePublished: site.published || FIRST_PUBLISHED, dateModified: today,
-- breadcrumb: { '@type': 'BreadcrumbList', itemListElement: [{ '@type': 'ListItem', position: 1, name: 'OpenVibe', item: `${NET.networkUrl}/` }, { '@type': 'ListItem', position: 2, name: site.name, item: url }] } },
-- ],
-- };
-- // A questions section people can read is also a FAQPage for search engines and AI crawlers.
-- if (site.faq && site.faq.length) ld['@graph'].push({ '@type': 'FAQPage', '@id': `${url}#faq`, mainEntity: site.faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })) });
-- return `<!DOCTYPE html>
--<html lang="en">
--<head>
--<meta charset="utf-8">
--<meta name="viewport" content="width=device-width, initial-scale=1">
--<title>${esc(title)}</title>
--<meta name="description" content="${esc(site.description)}">
--<meta name="keywords" content="${esc(site.keywords || '')}">
--<meta name="robots" content="${notice ? 'noindex, follow' : 'index, follow, max-image-preview:large'}">
--<meta name="theme-color" content="${esc(site.accent)}">
--<link rel="canonical" href="${url}">
--${require('openvibe-shared/app-icon').headTags({ site: 'network' }).split('\n')[0]}
--${require('openvibe-shared/app-icon').CRITICAL}
--<link rel="manifest" href="/manifest.webmanifest">
--<link rel="alternate" type="text/html" hreflang="en" href="${url}">
--<meta property="og:type" content="website">
--<meta property="og:site_name" content="${esc(site.name)}">
--<meta property="og:url" content="${url}">
--<meta property="og:title" content="${esc(title)}">
--<meta property="og:description" content="${esc(site.description)}">
--<meta property="og:image" content="${ogImage}">
--<meta property="og:image:alt" content="OpenVibe">
--<meta name="twitter:card" content="summary">
--<meta name="twitter:title" content="${esc(title)}">
--<meta name="twitter:description" content="${esc(site.description)}">
--<meta name="twitter:image" content="${ogImage}">
--<script type="application/ld+json">${JSON.stringify(ld)}</script>
--<script>(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script>
--<script src="${SHARED('theme-loader.js')}" defer></script>
--<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" media="print" onload="this.media='all'">
--<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"></noscript>
--<style>
--*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
--:root{--bg-primary:#0a0f1c;--bg-secondary:#101828;--bg-card:#131c2e;--bg-hover:#1c2a44;--border:#1f2d47;--text-primary:#e6edf7;--text-secondary:#96a7c2;--text-muted:#7386a3;--accent:#3b82f6;--accent-light:#60a5fa;--accent-rgb:59,130,246;--on-accent:#fff;--site:${esc(site.accent)};--site-rgb:${hexToRgb(site.accent)}}
--html{scroll-behavior:smooth}
--body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden}
--main{flex:1;position:relative}
--.orbs{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
--.orbs i{position:absolute;border-radius:50%;filter:blur(90px);opacity:.16;animation:drift 26s ease-in-out infinite alternate}
--.orbs i:nth-child(1){width:420px;height:420px;background:var(--site);top:-140px;left:-120px}
--.orbs i:nth-child(2){width:360px;height:360px;background:var(--accent);top:40%;right:-140px;animation-delay:-9s}
--.orbs i:nth-child(3){width:300px;height:300px;background:var(--site);bottom:-120px;left:38%;animation-delay:-17s}
--@keyframes drift{0%{transform:translate(0,0) scale(1)}50%{transform:translate(-26px,36px) scale(1.07)}100%{transform:translate(34px,-28px) scale(.95)}}
--.wrap{position:relative;z-index:1;max-width:1040px;margin:0 auto;padding:0 20px}
--.hero{padding:84px 0 44px;text-align:center}
--.hero .mark{display:inline-grid;place-items:center;width:84px;height:84px;border-radius:24px;background:rgba(var(--site-rgb),.12);border:1px solid rgba(var(--site-rgb),.35);color:var(--site);font-size:34px;margin-bottom:22px;box-shadow:0 20px 60px rgba(var(--site-rgb),.18);animation:rise .6s cubic-bezier(.2,.8,.2,1) both}
--.hero .badge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-size:12px;font-weight:600;color:var(--text-secondary);letter-spacing:.3px;margin-bottom:22px;animation:rise .6s .05s cubic-bezier(.2,.8,.2,1) both}
--.hero .badge b{color:var(--site)}
--.hero h1{font-size:clamp(34px,6vw,58px);font-weight:800;letter-spacing:-1.4px;line-height:1.08;animation:rise .6s .1s cubic-bezier(.2,.8,.2,1) both}
--.hero h1 .d{color:var(--site)}
--.hero h1 .tld{color:var(--text-secondary)}
--.hero .tag{font-size:clamp(18px,2.6vw,24px);font-weight:600;color:var(--text-primary);margin:18px 0 10px;animation:rise .6s .16s cubic-bezier(.2,.8,.2,1) both}
--.hero p.lead{color:var(--text-secondary);font-size:16px;line-height:1.65;max-width:680px;margin:0 auto;animation:rise .6s .22s cubic-bezier(.2,.8,.2,1) both}
--.ctas{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:30px;animation:rise .6s .3s cubic-bezier(.2,.8,.2,1) both}
--.hero .status{margin:22px auto 0;max-width:720px;font-size:12.5px;line-height:1.7;color:var(--text-muted);border:1px dashed var(--border);border-radius:12px;padding:10px 14px;background:rgba(0,0,0,.15)}
--.hero .status b{color:var(--site)}
--.hero .status code{font-size:12px;color:var(--text-secondary);background:var(--bg-secondary);padding:1px 6px;border-radius:6px}
--.hero .status a{color:var(--text-secondary)}
--.btn{display:inline-flex;align-items:center;gap:9px;padding:12px 20px;border-radius:12px;font-size:14px;font-weight:600;text-decoration:none;border:1px solid var(--border);color:var(--text-primary);background:var(--bg-secondary);transition:transform .15s,border-color .15s,box-shadow .15s}
--.btn:hover{transform:translateY(-1px);border-color:var(--site);box-shadow:0 10px 30px rgba(var(--site-rgb),.15)}
--.btn-primary{background:var(--site);border-color:var(--site);color:#0b0d10}
--.btn-primary:hover{filter:brightness(1.08)}
--@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
--section{padding:36px 0}
--h2{font-size:22px;font-weight:700;letter-spacing:-.3px;margin-bottom:6px}
--.lead2{color:var(--text-secondary);font-size:14px;margin-bottom:20px}
--.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
--.card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px;transition:transform .2s,border-color .2s,box-shadow .2s;text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:8px}
--a.card:hover{transform:translateY(-3px);border-color:rgba(var(--site-rgb),.6);box-shadow:0 16px 40px rgba(0,0,0,.35)}
--.card .ic{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;background:rgba(var(--site-rgb),.12);color:var(--site);font-size:17px}
--.card b{font-size:15px}
--.card span{font-size:13px;color:var(--text-secondary);line-height:1.55}
--.card .go{margin-top:auto;font-size:12.5px;font-weight:600;color:var(--site)}
--.highlight .hl-card{border:1px solid rgba(var(--accent-rgb),.45);background:rgba(var(--accent-rgb),.10);border-radius:18px;padding:22px 22px 18px}
--.highlight h2{margin:0 0 8px}.highlight h2 i{color:var(--accent-light)}.highlight p{margin:0 0 10px}
--.highlight ul{margin:0 0 14px;padding-left:20px}.highlight li{margin:4px 0}
--.faq details{border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px 14px;margin:8px 0}
--.faq summary{cursor:pointer;font-weight:600}.faq details p{margin:8px 0 0;opacity:.85}
--.meanwhile .card .ic{background:rgba(var(--accent-rgb),.12);color:var(--accent-light)}
--.net .card .ic{background:var(--bg-hover);color:var(--text-primary)}
--.rooms{display:flex;flex-wrap:wrap;gap:8px}
--.rooms a{font-size:12.5px;padding:7px 12px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary);text-decoration:none;transition:all .15s;background:var(--bg-secondary)}
--.rooms a:hover{color:var(--text-primary);border-color:var(--site)}
--.rooms a i{margin-right:6px;opacity:.8}
--.strip{border:1px solid var(--border);border-radius:18px;background:var(--bg-secondary);padding:30px 26px;text-align:center;margin:10px 0 50px}
--.strip h3{font-size:19px;font-weight:700;margin:12px 0 8px}
--.strip p{font-size:14px;color:var(--text-secondary);max-width:600px;margin:0 auto 18px;line-height:1.6}
--.strip .ov-mark{width:44px;height:44px}
--.hero.split{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:40px;align-items:center;text-align:left;padding-top:64px}
--.hero.split .ctas{justify-content:flex-start}.hero.split p.lead{margin:0}
--.viz{background:var(--bg-card);border:1px solid var(--border);border-radius:20px;padding:14px;box-shadow:0 30px 80px rgba(0,0,0,.45),0 0 0 1px rgba(var(--site-rgb),.08);animation:rise .7s .25s cubic-bezier(.2,.8,.2,1) both;text-align:left}
--.viz figcaption{margin-top:10px;font-size:11.5px;color:var(--text-muted);text-align:center}
--.viz-top{display:flex;align-items:center;gap:8px;font-size:13px;padding:2px 4px 12px}
--.viz-top b{font-weight:700}.viz-top .muted{color:var(--text-muted)}
--.viz-top .lat{margin-left:auto;font-size:12px;color:var(--site);background:rgba(var(--site-rgb),.1);border:1px solid rgba(var(--site-rgb),.3);padding:3px 9px;border-radius:999px}
--.viz-top .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;box-shadow:0 0 0 4px rgba(34,197,94,.18)}
--.viz-top .av{width:26px;height:26px;border-radius:8px;display:grid;place-items:center;background:rgba(var(--site-rgb),.15);color:var(--site);font-size:13px}
--.viz-video{position:relative;aspect-ratio:16/9;border-radius:14px;overflow:hidden;background:radial-gradient(120% 90% at 30% 20%,rgba(var(--site-rgb),.28),transparent 60%),linear-gradient(160deg,#1a2338,#0b1020 70%);border:1px solid var(--border)}
--.viz-video::after{content:'';position:absolute;left:10%;right:10%;bottom:0;height:42%;background:linear-gradient(transparent,rgba(0,0,0,.35)),repeating-linear-gradient(90deg,rgba(255,255,255,.05) 0 1px,transparent 1px 38px);transform:perspective(220px) rotateX(58deg);transform-origin:bottom}
--.viz-video .scan{position:absolute;inset:0;background:linear-gradient(transparent 0,rgba(255,255,255,.04) 50%,transparent 100%);background-size:100% 6px;opacity:.5}
--.viz-video .onair{position:absolute;top:10px;left:10px;font-size:11px;font-weight:700;padding:3px 8px;border-radius:6px;background:rgba(239,68,68,.9);color:#fff}.viz-video .onair i{font-size:7px;vertical-align:2px;margin-right:3px}
--.viz-video .cam{position:absolute;bottom:10px;right:10px;font-size:11px;color:#dbe4f5;background:rgba(0,0,0,.45);padding:3px 8px;border-radius:6px}
--.viz-ctrl{display:flex;gap:14px;align-items:center;margin-top:12px}
--.dpad{display:grid;grid-template-columns:repeat(3,34px);grid-template-rows:repeat(3,34px);gap:4px;flex:none}
--.dpad span{display:grid;place-items:center;border-radius:9px;background:var(--bg-hover);color:var(--text-primary);font-size:15px;border:1px solid var(--border)}
--.dpad .u{grid-area:1/2}.dpad .l{grid-area:2/1}.dpad .c{grid-area:2/2;background:rgba(var(--site-rgb),.15);border-color:rgba(var(--site-rgb),.35)}.dpad .r{grid-area:2/3}.dpad .d{grid-area:3/2}
--.dpad .u{background:rgba(var(--site-rgb),.22);border-color:rgba(var(--site-rgb),.5)}
--.viz-side{flex:1;min-width:0;display:grid;gap:8px}
--.viz-side .keys{display:flex;flex-wrap:wrap;gap:6px}
--.viz-side .keys span{font-size:11.5px;padding:5px 9px;border-radius:8px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
--.viz-side .meter{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--text-muted)}
--.viz-side .meter i{flex:1;height:6px;border-radius:9px;background:linear-gradient(90deg,#22c55e var(--v),var(--bg-hover) var(--v))}
--.viz-side .meter b{color:var(--text-secondary);font-weight:600}
--.viz-side .estop{justify-self:end;font-size:12px;font-weight:800;letter-spacing:.08em;color:#fff;background:#dc2626;border-radius:10px;padding:7px 16px;box-shadow:0 0 0 3px rgba(220,38,38,.25)}
--.feed{list-style:none;display:grid;gap:8px;padding:2px}
--.feed li{display:flex;align-items:flex-start;gap:10px;font-size:13.5px;line-height:1.45;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
--.feed li i{color:#22c55e;margin-top:3px}
--.feed li.ask{flex-wrap:wrap;border-color:rgba(var(--site-rgb),.45);background:rgba(var(--site-rgb),.08);color:var(--text-primary)}.feed li.ask i{color:var(--site)}.feed li.ask>span:first-of-type{flex:1;min-width:160px}
--.feed .pills{display:flex;gap:6px;margin-left:auto}.feed .pills span{font-size:12px;font-weight:600;padding:4px 11px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}.feed .pills .ok{background:var(--site);border-color:var(--site);color:#0b0d10}
--.steps{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;counter-reset:s}
--.steps li{position:relative;background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px 20px 20px;display:flex;flex-direction:column;gap:8px}
--.steps .n{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:14px;color:#0b0d10;background:var(--site)}
--.steps b{font-size:15px}.steps span:last-child{font-size:13px;color:var(--text-secondary);line-height:1.55}
--.grid.three{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
--.viz .task{font-size:14px;line-height:1.5;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary)}
--.viz .modes{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
--.viz .modes span{font-size:11.5px;font-weight:600;padding:4px 10px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}
--.viz .modes .on{background:var(--site);border-color:var(--site);color:#0b0d10}
--.cands{list-style:none;display:grid;gap:6px}
--.cands li{display:grid;grid-template-columns:18px minmax(0,1fr) 54px 50px 44px;align-items:center;gap:8px;font-size:12.5px;padding:8px 10px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary);position:relative}
--.cands li i{color:var(--text-muted)}.cands li b{font-weight:600;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.cands li span{text-align:right;font-variant-numeric:tabular-nums}
--.cands li.pick{border-color:rgba(var(--site-rgb),.55);background:rgba(var(--site-rgb),.09)}.cands li.pick i{color:var(--site)}
--.cands li em{position:absolute;top:-8px;right:10px;font-style:normal;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 7px;border-radius:999px;background:var(--site);color:#0b0d10}
--.viz .why{margin-top:10px;font-size:12.5px;line-height:1.5;color:var(--text-secondary)}.viz .why i{color:var(--site);margin-right:4px}
--.choice{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
--.card.big{padding:24px}.card.big b{font-size:17px}
--.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px}
--.chips span{font-size:12.5px;padding:6px 11px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-secondary)}
--.chips.big span{font-size:13.5px;padding:9px 14px;color:var(--text-primary)}.chips.big span i{color:var(--site);margin-right:8px}
--.build .card{border-style:dashed;background:rgba(0,0,0,.15)}.build .card b i{color:var(--site);margin-right:6px}.build .card span a{color:var(--text-secondary)}.build .card code{font-size:12px}
--@media (max-width:860px){.hero.split{grid-template-columns:1fr;text-align:center;gap:28px}.hero.split .ctas{justify-content:center}.hero.split p.lead{margin:0 auto}}
--@media (max-width:600px){.hero{padding-top:56px}.ctas .btn{width:100%;justify-content:center}.viz-ctrl{flex-direction:column;align-items:stretch}.dpad{align-self:center}.cands li{grid-template-columns:14px minmax(0,1fr) 30px 40px 30px;gap:6px;font-size:12px;padding:8px}}
--@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
--${site.statusApi ? STATUS_CSS : ''}</style>
--</head>
--<body>
--<div class="orbs"><i></i><i></i><i></i></div>
--<div id="navbar-mount"></div>
--<main class="wrap">
-- <header class="hero${site.vision ? ' split' : ''}">${site.vision ? '<div class="hero-main">' : ''}
-- <div class="mark"><i class="fa-solid ${esc(site.icon)}" aria-hidden="true"></i></div>
-- <div class="badge"><span class="ov-mark" data-size="16" data-variant="${esc(site.tld)}"></span> Part of the OpenVibe network · <b>${esc(site.vision ? site.vision.badge : STANDING[standing(site)].label)}</b></div>
-- <h1>${site.hostParts ? `<span class="d">${esc(site.hostParts[0])}</span><span class="tld">.${esc(site.hostParts[1])}</span>` : `${esc(core)}<span class="d">.</span><span class="tld">${esc(site.name.split('.').pop())}</span>`}</h1>
-- <div class="tag">${esc(site.tagline)}</div>
-- <p class="lead">${esc(site.description)}</p>
-- <div class="ctas">
-- ${primary ? `<a class="btn btn-primary" href="${esc(primary[1])}"><i class="fa-solid fa-arrow-right"></i> ${esc(primary[0])}</a>` : `<a class="btn btn-primary" href="${NET.networkUrl}/login?return=${encodeURIComponent(url)}"><i class="fa-solid fa-right-to-bracket"></i> Sign in with OpenVibe</a>`}
-- ${site.vision && site.vision.cta ? `<a class="btn" href="${esc(site.vision.cta[1])}"><i class="fa-solid ${esc(site.vision.cta[2])}"></i> ${esc(site.vision.cta[0])}</a>` : `<a class="btn" href="${NET.networkUrl}/#network"><i class="fa-solid fa-circle-nodes"></i> The whole network</a>`}
-- <a class="btn" href="${esc(NET.discord)}" rel="noopener"><i class="fa-brands fa-discord"></i> Follow the build</a>
-- </div>
-- ${site.vision ? `</div>
-- ${visionViz(site.vision)}` : `<p class="status"><i class="fa-solid fa-clock" aria-hidden="true"></i> Status: <b>${esc(STANDING[standing(site)].label)}</b> · ${statusText(site)} · as of ${esc(facts.generated)} · <a href="/status.json">status.json</a></p>`}
-- </header>
--
--${site.highlight ? highlight(site.highlight) : ''}
--${notice ? ` <section id="what" class="meanwhile">
-- <h2>Where to go</h2>
-- <p class="lead2">${esc(site.linksLead || 'What this address used to stand for lives here now.')}</p>
-- <div class="grid">
-- ${site.links.map(([h, href, d]) => `<a class="card" href="${esc(href)}"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>${esc(h)}</b><span>${esc(d)}</span><span class="go">${esc(href.replace(/^https?:\/\//, '').replace(/\/$/, ''))} →</span></a>`).join('\n ')}
-- </div>
-- </section>
--${site.statusApi ? liveStatus(site) : ''}` : site.vision ? vision(site) : ` <section id="what">
-- <h2>What ${esc(site.name)} will be</h2>
-- <p class="lead2">Same account, same themes, same navbar as every other OpenVibe site — built in the open, run by its community.</p>
-- <div class="grid">
-- ${site.pillars.map(([h, d]) => `<div class="card"><div class="ic"><i class="fa-solid ${esc(site.icon)}" aria-hidden="true"></i></div><b>${esc(h)}</b><span>${esc(d)}</span></div>`).join('\n ')}
-- </div>
-- </section>
--
-- <section class="meanwhile">
-- <h2>Meanwhile, on the network</h2>
-- <p class="lead2">Most of this already exists somewhere on OpenVibe — start there today.</p>
-- <div class="grid">
-- ${(site.meanwhile || []).map(([h, href, d]) => `<a class="card" href="${esc(href)}"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>${esc(h)}</b><span>${esc(d)}</span><span class="go">${esc(href.replace(/^https?:\/\//, '').replace(/\/$/, ''))} →</span></a>`).join('\n ')}
-- </div>
-- </section>
--${site.faq && site.faq.length ? `
-- <section id="questions" class="faq">
-- <h2>Questions</h2>
-- ${site.faq.map(([q, ans]) => `<details><summary>${esc(q)}</summary><p>${esc(ans)}</p></details>`).join('\n ')}
-- </section>` : ''}
--`}
-- <section class="net">
-- <h2>The OpenVibe network</h2>
-- <p class="lead2">One account for all of it. Sign in once and every site signs you in.</p>
-- <div class="grid">
-- ${others.map(s => `<a class="card" href="${esc(s.url)}"><div class="ic"><i class="fa-solid ${esc(s.icon)}" aria-hidden="true"></i></div><b>${esc(s.name)}</b><span>${esc(s.desc)}</span><span class="go">${esc(s.url.replace(/^https?:\/\//, ''))} →</span></a>`).join('\n ')}
-- </div>
-- <h2 style="margin-top:28px;font-size:16px">Other OpenVibe domains, not open yet</h2>
-- <div class="rooms" style="margin-top:8px">
-- ${siblings.map(s => `<a href="https://${esc(s.domain)}/"><i class="fa-solid ${esc(s.icon)}" aria-hidden="true"></i>${esc(s.name)}</a>`).join('\n ')}
-- </div>
-- </section>
--
-- <div class="strip">
-- <span class="ov-mark" data-size="44" data-variant="${esc(site.tld)}"></span>
-- <h3>Open source. Community run.</h3>
-- <p>Every OpenVibe site is built in the open by the people using it — free speech within the rules, no gatekeeping, one identity that follows you everywhere.</p>
-- <a class="btn" href="${esc(NET.github)}" rel="noopener"><i class="fa-brands fa-github"></i> OpenVibers on GitHub</a>
-- </div>
--</main>
--${require('openvibe-shared/frame').footer({ service: site.tld, variant: 'full' })}
--<script src="${SHARED('ov-mark.js')}" async></script>
--<script src="${SHARED('navbar.js')}"></script>
--<script src="${SHARED('footer.js')}"></script>
--<script>
--(function () {
-- if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: '${esc(site.tld)}', apiBase: '${NET.networkUrl}', links: [{ label: '${notice ? 'Where to go' : site.vision ? 'How it works' : 'What it will be'}', href: '#what' }, { label: 'The network', href: '${NET.networkUrl}/#network', external: false }], history: { type: 'page', title: '${esc(site.name)}' } }); } catch (e) {} }
-- if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: '${esc(site.tld)}', variant: 'full', links: [{ heading: '${esc(site.name)}', items: [{ name: 'Sign in', url: '${NET.networkUrl}/login?return=${encodeURIComponent(url)}' }, { name: 'The network', url: '${NET.networkUrl}/#network' }, { name: 'Discord', url: '${esc(NET.discord)}' }] }] }); } catch (e) {} }
--})();
--</script>
--</body>
--</html>
--`;
--}
++const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
++const legalSite = (site) => ({ id: site.tld, service: 'network', host: site.domain, name: site.name, profile: site.legalProfile || 'info' });
--// A notice page is noindex and has no sitemap; a placeholder lists its one page.
--function robots(site) { return `User-agent: *\nAllow: /\nDisallow: /auth/\n${site.kind ? '' : `Sitemap: https://${site.domain}/sitemap.xml\n`}`; }
++// Presentation is owned by the product teams. These notice snapshots preserve the existing pages.
++function page(site) { return fs.readFileSync(path.join(ROOT, 'notice-pages', `${site.domain}.html`), 'utf8'); }
++function robots() { return 'User-agent: *\nAllow: /\nDisallow: /auth/\n'; }
function sitemap(site) { return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://${site.domain}/</loc><lastmod>${today}</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>\n</urlset>\n`; }
function manifest(site) { return JSON.stringify(require('openvibe-shared/app-icon').manifest({ site: 'network', name: site.name, shortName: site.name.split('.').pop(), description: site.tagline, iconBase: `${NET.networkUrl}/assets` }), null, 2) + '\n'; }
@@@ -541,7 +85,7 @@ server {
return 301 https://${d}$request_uri;
}
`;
-- return `# ${d} — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++ return `# ${d} — static front page (OpenVibe.Sites). Generated by build.js; edit notices.json, not this.
server {
listen 80;
listen [::]:80;
@@@ -621,126 +165,65 @@ server {
`;
}
--const STATUS_CSS = `.live .sum{font-size:14px;color:var(--text-secondary);margin-bottom:14px;line-height:1.6}
--.live .sum b{color:var(--text-primary)}
--.live .tbl{overflow-x:auto;border:1px solid var(--border);border-radius:14px;background:var(--bg-card)}
--.live table{width:100%;border-collapse:collapse;font-size:13px}
--.live th,.live td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
--.live th{color:var(--text-muted);font-weight:600;font-size:12px}
--.live tr:last-child td{border-bottom:0}
--.live td.st-up{color:#4ade80}.live td.st-degraded{color:#facc15}.live td.st-down{color:#f87171}.live td.st-not-running,.live td.st-unknown{color:var(--text-muted)}
--`;
--/**
-- * "Right now" on the status pointer: the summary and rows of Network's health poll, read in the browser
-- * when the page opens from /api/v1/registry/health (public discovery: CORS-open to any origin, unlike
-- * /api/v1/status, which keeps Network's first-party allow-list). Labels follow Network's status page: a
-- * service that is up but not public reads "up (loopback only)", never a bare "up". Built with DOM nodes
-- * (no innerHTML); without JavaScript, or when the API does not answer, the page says so and links the
-- * status page.
-- */
--function liveStatus(site) {
-- return `
-- <section class="live" aria-live="polite">
-- <h2>Right now</h2>
-- <p class="lead2">Read from <a href="${esc(site.statusApi)}" style="color:inherit">${esc(site.statusApi.replace(/^https?:\/\//, ''))}</a> when this page opened.</p>
-- <p class="sum" id="live-sum">Open the <a href="${esc(site.links[0][1])}" style="color:inherit">status page</a> for the current state of every service.</p>
-- <div class="tbl" id="live-tbl" hidden><table><thead><tr><th>Service</th><th>Status</th><th>Where it runs</th></tr></thead><tbody id="live-rows"></tbody></table></div>
-- </section>
--<script>
--(function () {
-- var sum = document.getElementById('live-sum'), tbl = document.getElementById('live-tbl'), rows = document.getElementById('live-rows');
-- if (!sum || !window.fetch) return;
-- var WHERE = { live: 'public', internal: 'loopback only, no public site yet', library: 'library (nothing to run)', repository: 'repository (nothing running)', placeholder: 'planned (nothing running)' };
-- function el(tag, text, cls) { var n = document.createElement(tag); if (text != null) n.textContent = String(text); if (cls) n.className = cls; return n; }
-- fetch('${esc(site.statusApi)}', { headers: { accept: 'application/json' } }).then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }).then(function (d) {
-- var s = d.summary || {}, order = ['up', 'degraded', 'down', 'not-running', 'unknown'], parts = [];
-- sum.textContent = '';
-- order.forEach(function (k) { if (s[k] == null) return; var b = el('b', s[k]); var w = document.createDocumentFragment(); w.appendChild(b); w.appendChild(document.createTextNode(' ' + k.replace('-', ' '))); parts.push(w); });
-- parts.forEach(function (p, i) { if (i) sum.appendChild(document.createTextNode(' · ')); sum.appendChild(p); });
-- var at = d.checked_at;
-- if (at) sum.appendChild(document.createTextNode(' · checked ' + new Date(at).toUTCString().replace(/ GMT$/, ' UTC')));
-- (d.services || []).forEach(function (x) {
-- var tr = document.createElement('tr');
-- var st = String(x.status || 'unknown');
-- tr.appendChild(el('td', x.id));
-- tr.appendChild(el('td', (st === 'up' && x.state !== 'live' ? 'up (loopback only)' : st.replace('-', ' ')) + (x.stale ? ' (stale)' : ''), 'st-' + st));
-- tr.appendChild(el('td', WHERE[x.state] || x.state || 'unknown'));
-- rows.appendChild(tr);
-- });
-- if (rows.children.length) tbl.hidden = false;
-- }).catch(function () { sum.textContent = 'The status API did not answer just now. Try the status page itself: ${esc(site.links[0][1])}'; });
--})();
--</script>
--`;
--}
--
--/**
-- * /release.json: what the shared release watcher (openvibe-shared release-watch, loaded by the navbar
-- * on every page) asks each site for. A placeholder's release is the hash of its page with the dates
-- * neutralised, so it changes exactly when the page does.
-- */
function releaseJson(site, html) {
-- const release = require('crypto').createHash('sha256').update(String(html).replace(/\d{4}-\d{2}-\d{2}/g, 'DATE')).digest('hex').slice(0, 12);
++ const release = crypto.createHash('sha256').update(String(html).replace(/\d{4}-\d{2}-\d{2}/g, 'DATE')).digest('hex').slice(0, 12);
return JSON.stringify({
-- service: site.tld, release, released_at: `${today}T00:00:00.000Z`, booted_at: null, contracts_version: null,
-- packages: { 'openvibe-shared': require('openvibe-shared/package.json').version },
-- min_client_release: null, mixed_version_window_hours: 24, kind: 'placeholder',
++ service: site.tld, release, released_at: `${today}T00:00:00.000Z`, booted_at: null,
++ contracts_version: null, packages: { 'openvibe-shared': require('openvibe-shared/package.json').version },
++ min_client_release: null, mixed_version_window_hours: 24, kind: 'notice',
}, null, 2) + '\n';
}
--
--/** /status.json: the same facts as the page, for scripts (CORS-open). */
function statusJson(site) {
-- const f = repoFacts(site);
-- const k = standing(site);
-- const st = (f && f.status) || null; const rg = (f && f.registry) || null;
return JSON.stringify({
-- domain: site.domain, name: site.name, stage: site.kind || 'placeholder', live: false,
-- plannedRepo: site.plannedRepo ? `OpenVibers/${site.plannedRepo}` : null,
-- repoUrl: site.plannedRepo ? `https://github.com/OpenVibers/${site.plannedRepo}` : null,
-- repoExists: !!site.plannedRepo,
-- repoStage: STANDING[k].repoStage,
-- service: f ? { stage: st && st.stage, code: st ? st.code : null, deployed: st ? st.deployed : null, statusCommit: st && st.commit, exposure: rg && rg.state, publicOrigin: rg && rg.origin } : null,
-- launchWaitsFor: site.launch || null,
-- factsAsOf: facts.generated,
++ domain: site.domain, name: site.name, stage: site.kind, live: false,
network: NET.networkUrl, updated: today,
-- note: 'Static page served by OpenVibers/OpenVibe.Sites; service facts come from each repository\'s STATUS.json and the Network registry (facts.json). Removed from sites.json in the same release that the real service takes over this domain.',
++ note: 'Static notice served by OpenVibers/OpenVibe.Sites. See the linked destination for current information.',
}, null, 2) + '\n';
}
--
function build() {
const out = {};
-- for (const site of catalog.sites) {
++ for (const site of notices) {
out[`${site.domain}/index.html`] = page(site);
-- out[`${site.domain}/release.json`] = releaseJson(site, out[`${site.domain}/index.html`]);
out[`${site.domain}/404.html`] = notFound(site);
out[`${site.domain}/robots.txt`] = robots(site);
-- if (!site.kind) out[`${site.domain}/sitemap.xml`] = sitemap(site);
out[`${site.domain}/manifest.webmanifest`] = manifest(site);
++ out[`${site.domain}/release.json`] = releaseJson(site, out[`${site.domain}/index.html`]);
out[`${site.domain}/status.json`] = statusJson(site);
for (const kind of ['terms', 'privacy', 'dmca']) out[`${site.domain}/${kind}.html`] = legal.page(kind, legalSite(site));
-- // A site whose service repository owns the vhost keeps its pages here but its nginx config ships with that service.
if (!site.vhostOwner) out[`../deploy/nginx/${site.domain}.conf`] = vhost(site);
}
-- for (const g of LEGAL_ONLY) for (const kind of ['terms', 'privacy', 'dmca']) out[`${g.domain}/${kind}.html`] = legal.page(kind, { id: g.id, service: g.id, host: g.domain, name: g.name, profile: g.profile });
-- // One copy of every browser file of the pinned openvibe-shared (the pages' scripts and what they load beside them).
for (const name of sharedFiles.BROWSER) out[`_shared/${name}`] = fs.readFileSync(sharedFiles.path(name), 'utf8');
return out;
}
--
--const files = build();
--if (process.argv.includes('--check')) {
-- let stale = 0;
-- for (const [rel, content] of Object.entries(files)) {
++function verifyFrozen() {
++ const noticeDomains = new Set(notices.map(site => site.domain));
++ let missing = 0;
++ for (const domain of frozen) {
++ if (noticeDomains.has(domain)) { console.error(`frozen domain is also a notice: ${domain}`); missing++; }
++ // Bot's own repository supplies its vhost; Sites keeps only its frozen files.
++ const files = [`dist/${domain}/index.html`, ...(domain === 'openvibe.bot' ? [] : [`deploy/nginx/${domain}.conf`])];
++ for (const file of files) {
++ if (!fs.existsSync(path.join(ROOT, file))) { console.error(`missing frozen file: ${file}`); missing++; }
++ }
++ }
++ return missing;
++}
++function check() {
++ let stale = verifyFrozen();
++ const norm = (s) => String(s).replace(/\d{4}-\d{2}-\d{2}/g, 'DATE');
++ for (const [rel, content] of Object.entries(build())) {
const p = path.join(DIST, rel);
-- // lastmod/datePublished change daily; compare with dates neutralised.
-- const norm = (s) => String(s).replace(/\d{4}-\d{2}-\d{2}/g, 'DATE');
if (!fs.existsSync(p) || norm(fs.readFileSync(p, 'utf8')) !== norm(content)) { console.error(`stale: ${rel}`); stale++; }
}
-- process.exit(stale ? 1 : 0);
++ return stale;
}
--for (const [rel, content] of Object.entries(files)) {
-- const p = path.join(DIST, rel);
-- fs.mkdirSync(path.dirname(p), { recursive: true });
-- fs.writeFileSync(p, content);
++if (require.main === module) {
++ if (process.argv.includes('--check')) process.exit(check() ? 1 : 0);
++ for (const [rel, content] of Object.entries(build())) {
++ const p = path.join(DIST, rel);
++ fs.mkdirSync(path.dirname(p), { recursive: true });
++ fs.writeFileSync(p, content);
++ }
++ console.log(`built ${notices.length} notices; preserved ${frozen.length} frozen product domains`);
}
--console.log(`built ${catalog.sites.length} sites → dist/`);
++module.exports = { build, check, verifyFrozen };
ddiff --git a/deploy/nginx/admin.openvibe.network.conf b/deploy/nginx/admin.openvibe.network.conf
iindex 1149181..3924a75 100644
---- a/deploy/nginx/admin.openvibe.network.conf
++++ b/deploy/nginx/admin.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# admin.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# admin.openvibe.network — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/ai.openvibe.network.conf b/deploy/nginx/ai.openvibe.network.conf
iindex 2ece4da..5047b08 100644
---- a/deploy/nginx/ai.openvibe.network.conf
++++ b/deploy/nginx/ai.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# ai.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# ai.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit notices.json, not this.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/ai.openvibe.services.conf b/deploy/nginx/ai.openvibe.services.conf
iindex c2840a1..03b4108 100644
---- a/deploy/nginx/ai.openvibe.services.conf
++++ b/deploy/nginx/ai.openvibe.services.conf
@@@ -1,4 +1,4 @@
--# ai.openvibe.services — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# ai.openvibe.services — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/api.openvibe.network.conf b/deploy/nginx/api.openvibe.network.conf
iindex 324665e..5257c12 100644
---- a/deploy/nginx/api.openvibe.network.conf
++++ b/deploy/nginx/api.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# api.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# api.openvibe.network — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/auth.openvibe.network.conf b/deploy/nginx/auth.openvibe.network.conf
iindex 6ddabe7..530253a 100644
---- a/deploy/nginx/auth.openvibe.network.conf
++++ b/deploy/nginx/auth.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# auth.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# auth.openvibe.network — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openre.stream.conf b/deploy/nginx/openre.stream.conf
iindex 3fc81d7..df3b9e9 100644
---- a/deploy/nginx/openre.stream.conf
++++ b/deploy/nginx/openre.stream.conf
@@@ -1,4 +1,4 @@
--# openre.stream — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openre.stream — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.actor.conf b/deploy/nginx/openvibe.actor.conf
iindex 873a5a1..a1f63e3 100644
---- a/deploy/nginx/openvibe.actor.conf
++++ b/deploy/nginx/openvibe.actor.conf
@@@ -1,4 +1,4 @@
--# openvibe.actor — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.actor — static front page (OpenVibe.Sites). Generated by build.js; edit notices.json, not this.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.coupons.conf b/deploy/nginx/openvibe.coupons.conf
iindex e3c5b39..b17f04e 100644
---- a/deploy/nginx/openvibe.coupons.conf
++++ b/deploy/nginx/openvibe.coupons.conf
@@@ -1,4 +1,4 @@
--# openvibe.coupons — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.coupons — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.deals.conf b/deploy/nginx/openvibe.deals.conf
iindex 1395f9f..2bff0e3 100644
---- a/deploy/nginx/openvibe.deals.conf
++++ b/deploy/nginx/openvibe.deals.conf
@@@ -1,4 +1,4 @@
--# openvibe.deals — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.deals — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.download.conf b/deploy/nginx/openvibe.download.conf
iindex c30a1a4..335eba8 100644
---- a/deploy/nginx/openvibe.download.conf
++++ b/deploy/nginx/openvibe.download.conf
@@@ -1,4 +1,4 @@
--# openvibe.download — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.download — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.events.conf b/deploy/nginx/openvibe.events.conf
iindex b5ced9c..408e4db 100644
---- a/deploy/nginx/openvibe.events.conf
++++ b/deploy/nginx/openvibe.events.conf
@@@ -1,4 +1,4 @@
--# openvibe.events — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.events — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.food.conf b/deploy/nginx/openvibe.food.conf
iindex 9b643eb..46f5c84 100644
---- a/deploy/nginx/openvibe.food.conf
++++ b/deploy/nginx/openvibe.food.conf
@@@ -1,4 +1,4 @@
--# openvibe.food — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.food — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.help.conf b/deploy/nginx/openvibe.help.conf
iindex f31dd7a..30beb15 100644
---- a/deploy/nginx/openvibe.help.conf
++++ b/deploy/nginx/openvibe.help.conf
@@@ -1,4 +1,4 @@
--# openvibe.help — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.help — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.homes.conf b/deploy/nginx/openvibe.homes.conf
iindex c59f23a..470029d 100644
---- a/deploy/nginx/openvibe.homes.conf
++++ b/deploy/nginx/openvibe.homes.conf
@@@ -1,4 +1,4 @@
--# openvibe.homes — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.homes — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.host.conf b/deploy/nginx/openvibe.host.conf
iindex 84e7111..8045cfe 100644
---- a/deploy/nginx/openvibe.host.conf
++++ b/deploy/nginx/openvibe.host.conf
@@@ -1,4 +1,4 @@
--# openvibe.host — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.host — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.news.conf b/deploy/nginx/openvibe.news.conf
iindex 4d0ac24..cee0d95 100644
---- a/deploy/nginx/openvibe.news.conf
++++ b/deploy/nginx/openvibe.news.conf
@@@ -1,4 +1,4 @@
--# openvibe.news — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.news — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.pics.conf b/deploy/nginx/openvibe.pics.conf
iindex 6bf0680..9cebfc2 100644
---- a/deploy/nginx/openvibe.pics.conf
++++ b/deploy/nginx/openvibe.pics.conf
@@@ -1,4 +1,4 @@
--# openvibe.pics — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.pics — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.quest.conf b/deploy/nginx/openvibe.quest.conf
iindex d04949a..8bb60a5 100644
---- a/deploy/nginx/openvibe.quest.conf
++++ b/deploy/nginx/openvibe.quest.conf
@@@ -1,4 +1,4 @@
--# openvibe.quest — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.quest — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.rent.conf b/deploy/nginx/openvibe.rent.conf
iindex 366ccbe..a0d7689 100644
---- a/deploy/nginx/openvibe.rent.conf
++++ b/deploy/nginx/openvibe.rent.conf
@@@ -1,4 +1,4 @@
--# openvibe.rent — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.rent — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.reviews.conf b/deploy/nginx/openvibe.reviews.conf
iindex 6dd7932..660dc32 100644
---- a/deploy/nginx/openvibe.reviews.conf
++++ b/deploy/nginx/openvibe.reviews.conf
@@@ -1,4 +1,4 @@
--# openvibe.reviews — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.reviews — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.run.conf b/deploy/nginx/openvibe.run.conf
iindex 5f1327b..245a01d 100644
---- a/deploy/nginx/openvibe.run.conf
++++ b/deploy/nginx/openvibe.run.conf
@@@ -1,4 +1,4 @@
--# openvibe.run — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.run — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.services.conf b/deploy/nginx/openvibe.services.conf
iindex bb2bb81..4f4423a 100644
---- a/deploy/nginx/openvibe.services.conf
++++ b/deploy/nginx/openvibe.services.conf
@@@ -1,4 +1,4 @@
--# openvibe.services — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.services — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.space.conf b/deploy/nginx/openvibe.space.conf
iindex 36c06a2..b09f418 100644
---- a/deploy/nginx/openvibe.space.conf
++++ b/deploy/nginx/openvibe.space.conf
@@@ -1,4 +1,4 @@
--# openvibe.space — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.space — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.tips.conf b/deploy/nginx/openvibe.tips.conf
iindex 271c277..48099f7 100644
---- a/deploy/nginx/openvibe.tips.conf
++++ b/deploy/nginx/openvibe.tips.conf
@@@ -1,4 +1,4 @@
--# openvibe.tips — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.tips — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.trade.conf b/deploy/nginx/openvibe.trade.conf
iindex 16d931a..bf3c1c0 100644
---- a/deploy/nginx/openvibe.trade.conf
++++ b/deploy/nginx/openvibe.trade.conf
@@@ -1,4 +1,4 @@
--# openvibe.trade — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.trade — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.video.conf b/deploy/nginx/openvibe.video.conf
iindex 0826b84..4e2d966 100644
---- a/deploy/nginx/openvibe.video.conf
++++ b/deploy/nginx/openvibe.video.conf
@@@ -1,4 +1,4 @@
--# openvibe.video — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.video — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.vip.conf b/deploy/nginx/openvibe.vip.conf
iindex af10ff6..baea322 100644
---- a/deploy/nginx/openvibe.vip.conf
++++ b/deploy/nginx/openvibe.vip.conf
@@@ -1,4 +1,4 @@
--# openvibe.vip — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.vip — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.watch.conf b/deploy/nginx/openvibe.watch.conf
iindex 35560f4..a4f818d 100644
---- a/deploy/nginx/openvibe.watch.conf
++++ b/deploy/nginx/openvibe.watch.conf
@@@ -1,4 +1,4 @@
--# openvibe.watch — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.watch — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.website.conf b/deploy/nginx/openvibe.website.conf
iindex f1e0b8d..16c43ca 100644
---- a/deploy/nginx/openvibe.website.conf
++++ b/deploy/nginx/openvibe.website.conf
@@@ -1,4 +1,4 @@
--# openvibe.website — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.website — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.work.conf b/deploy/nginx/openvibe.work.conf
iindex c867bff..12ff0fc 100644
---- a/deploy/nginx/openvibe.work.conf
++++ b/deploy/nginx/openvibe.work.conf
@@@ -1,4 +1,4 @@
--# openvibe.work — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.work — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/openvibe.zone.conf b/deploy/nginx/openvibe.zone.conf
iindex e021ecc..d91aca8 100644
---- a/deploy/nginx/openvibe.zone.conf
++++ b/deploy/nginx/openvibe.zone.conf
@@@ -1,4 +1,4 @@
--# openvibe.zone — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# openvibe.zone — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/realtime.openvibe.network.conf b/deploy/nginx/realtime.openvibe.network.conf
iindex 04d72b7..9a3a26b 100644
---- a/deploy/nginx/realtime.openvibe.network.conf
++++ b/deploy/nginx/realtime.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# realtime.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# realtime.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit notices.json, not this.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/status.openvibe.network.conf b/deploy/nginx/status.openvibe.network.conf
iindex 73a5299..d51a419 100644
---- a/deploy/nginx/status.openvibe.network.conf
++++ b/deploy/nginx/status.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# status.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# status.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit notices.json, not this.
server {
listen 80;
listen [::]:80;
ddiff --git a/deploy/nginx/themes.openvibe.network.conf b/deploy/nginx/themes.openvibe.network.conf
iindex 512f668..acf3f0a 100644
---- a/deploy/nginx/themes.openvibe.network.conf
++++ b/deploy/nginx/themes.openvibe.network.conf
@@@ -1,4 +1,4 @@
--# themes.openvibe.network — static front page (OpenVibe.Sites). Generated by build.js; edit sites.json, not this.
++# themes.openvibe.network — static front page (OpenVibe.Sites). Frozen static vhost; owner cutover pending.
server {
listen 80;
listen [::]:80;
ddiff --git a/dist/ai.openvibe.network/release.json b/dist/ai.openvibe.network/release.json
iindex 49f8c28..222d2c2 100644
---- a/dist/ai.openvibe.network/release.json
++++ b/dist/ai.openvibe.network/release.json
@@@ -1,7 +1,7 @@
{
"service": "ai",
"release": "cf9bea90d600",
-- "released_at": "2026-10-03T00:00:00.000Z",
++ "released_at": "2026-10-04T00:00:00.000Z",
"booted_at": null,
"contracts_version": null,
"packages": {
@@@ -9,5 +9,5 @@
},
"min_client_release": null,
"mixed_version_window_hours": 24,
-- "kind": "placeholder"
++ "kind": "notice"
}
ddiff --git a/dist/ai.openvibe.network/status.json b/dist/ai.openvibe.network/status.json
iindex ba95ad6..a19e35f 100644
---- a/dist/ai.openvibe.network/status.json
++++ b/dist/ai.openvibe.network/status.json
@@@ -3,21 +3,7 @@
"name": "ai.openvibe.network",
"stage": "moved",
"live": false,
-- "plannedRepo": "OpenVibers/OpenVibe.AI",
-- "repoUrl": "https://github.com/OpenVibers/OpenVibe.AI",
-- "repoExists": true,
-- "repoStage": "service-running",
-- "service": {
-- "stage": "alpha",
-- "code": true,
-- "deployed": true,
-- "statusCommit": "0c98c51",
-- "exposure": "internal",
-- "publicOrigin": null
-- },
-- "launchWaitsFor": null,
-- "factsAsOf": "2026-09-29",
"network": "https://openvibe.network",
-- "updated": "2026-10-03",
-- "note": "Static page served by OpenVibers/OpenVibe.Sites; service facts come from each repository's STATUS.json and the Network registry (facts.json). Removed from sites.json in the same release that the real service takes over this domain."
++ "updated": "2026-10-04",
++ "note": "Static notice served by OpenVibers/OpenVibe.Sites. See the linked destination for current information."
}
ddiff --git a/dist/realtime.openvibe.network/release.json b/dist/realtime.openvibe.network/release.json
iindex 979760a..87aee3d 100644
---- a/dist/realtime.openvibe.network/release.json
++++ b/dist/realtime.openvibe.network/release.json
@@@ -1,7 +1,7 @@
{
"service": "realtime",
"release": "5e7a2635028e",
-- "released_at": "2026-10-03T00:00:00.000Z",
++ "released_at": "2026-10-04T00:00:00.000Z",
"booted_at": null,
"contracts_version": null,
"packages": {
@@@ -9,5 +9,5 @@
},
"min_client_release": null,
"mixed_version_window_hours": 24,
-- "kind": "placeholder"
++ "kind": "notice"
}
ddiff --git a/dist/realtime.openvibe.network/status.json b/dist/realtime.openvibe.network/status.json
iindex 102dd51..f7ba7e0 100644
---- a/dist/realtime.openvibe.network/status.json
++++ b/dist/realtime.openvibe.network/status.json
@@@ -3,14 +3,7 @@
"name": "realtime.openvibe.network",
"stage": "closed",
"live": false,
-- "plannedRepo": null,
-- "repoUrl": null,
-- "repoExists": false,
-- "repoStage": "closed",
-- "service": null,
-- "launchWaitsFor": null,
-- "factsAsOf": "2026-09-29",
"network": "https://openvibe.network",
-- "updated": "2026-10-03",
-- "note": "Static page served by OpenVibers/OpenVibe.Sites; service facts come from each repository's STATUS.json and the Network registry (facts.json). Removed from sites.json in the same release that the real service takes over this domain."
++ "updated": "2026-10-04",
++ "note": "Static notice served by OpenVibers/OpenVibe.Sites. See the linked destination for current information."
}
ddiff --git a/dist/status.openvibe.network/release.json b/dist/status.openvibe.network/release.json
iindex 25b4531..436721e 100644
---- a/dist/status.openvibe.network/release.json
++++ b/dist/status.openvibe.network/release.json
@@@ -1,7 +1,7 @@
{
"service": "status",
"release": "717343d5e95a",
-- "released_at": "2026-10-03T00:00:00.000Z",
++ "released_at": "2026-10-04T00:00:00.000Z",
"booted_at": null,
"contracts_version": null,
"packages": {
@@@ -9,5 +9,5 @@
},
"min_client_release": null,
"mixed_version_window_hours": 24,
-- "kind": "placeholder"
++ "kind": "notice"
}
ddiff --git a/dist/status.openvibe.network/status.json b/dist/status.openvibe.network/status.json
iindex 1aea867..f10bda8 100644
---- a/dist/status.openvibe.network/status.json
++++ b/dist/status.openvibe.network/status.json
@@@ -3,21 +3,7 @@
"name": "status.openvibe.network",
"stage": "status",
"live": false,
-- "plannedRepo": "OpenVibers/OpenVibe.Network",
-- "repoUrl": "https://github.com/OpenVibers/OpenVibe.Network",
-- "repoExists": true,
-- "repoStage": "pointer",
-- "service": {
-- "stage": "stable",
-- "code": true,
-- "deployed": true,
-- "statusCommit": "5ca28e2",
-- "exposure": "live",
-- "publicOrigin": "https://openvibe.network"
-- },
-- "launchWaitsFor": null,
-- "factsAsOf": "2026-09-29",
"network": "https://openvibe.network",
-- "updated": "2026-10-03",
-- "note": "Static page served by OpenVibers/OpenVibe.Sites; service facts come from each repository's STATUS.json and the Network registry (facts.json). Removed from sites.json in the same release that the real service takes over this domain."
++ "updated": "2026-10-04",
++ "note": "Static notice served by OpenVibers/OpenVibe.Sites. See the linked destination for current information."
}
ddiff --git a/frozen.json b/frozen.json
nnew file mode 100644
iindex 0000000..35beedf
---- /dev/null
++++ b/frozen.json
@@@ -0,0 +1,34 @@
++[
++ "openvibe.news",
++ "openvibe.reviews",
++ "openvibe.tips",
++ "openvibe.vip",
++ "openvibe.trade",
++ "openvibe.host",
++ "openvibe.deals",
++ "openvibe.coupons",
++ "openre.stream",
++ "auth.openvibe.network",
++ "api.openvibe.network",
++ "admin.openvibe.network",
++ "themes.openvibe.network",
++ "ai.openvibe.services",
++ "openvibe.pics",
++ "openvibe.download",
++ "openvibe.space",
++ "openvibe.food",
++ "openvibe.quest",
++ "openvibe.rent",
++ "openvibe.homes",
++ "openvibe.services",
++ "openvibe.run",
++ "openvibe.work",
++ "openvibe.zone",
++ "openvibe.video",
++ "openvibe.website",
++ "openvibe.help",
++ "openvibe.watch",
++ "openvibe.events",
++ "openvibe.actor",
++ "openvibe.bot"
++]
ddiff --git a/notice-pages/ai.openvibe.network.html b/notice-pages/ai.openvibe.network.html
nnew file mode 100644
iindex 0000000..119d9a0
---- /dev/null
++++ b/notice-pages/ai.openvibe.network.html
@@@ -0,0 +1,332 @@
++<!DOCTYPE html>
++<html lang="en">
++<head>
++<meta charset="utf-8">
++<meta name="viewport" content="width=device-width, initial-scale=1">
++<title>ai.openvibe.network — Moved: OpenVibe.AI&#39;s public home is ai.openvibe.services.</title>
++<meta name="description" content="OpenVibe.AI&#39;s public address is now ai.openvibe.services, next to the rest of the network&#39;s programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.">
++<meta name="keywords" content="openvibe ai, openvibe network ai, openvibe platform">
++<meta name="robots" content="noindex, follow">
++<meta name="theme-color" content="#c084fc">
++<link rel="canonical" href="https://ai.openvibe.network/">
++<link rel="icon" type="image/svg+xml" data-ov-icon="network" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-32%20-32%2064%2064%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22ring%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2360a5fa%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%232563eb%22%2F%3E%3C%2FlinearGradient%3E%0A%3CradialGradient%20id%3D%22glow%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2360a5fa%22%20stop-opacity%3D%22.34%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2360a5fa%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%0A%3CradialGradient%20id%3D%22bg%22%20cx%3D%22.5%22%20cy%3D%22.38%22%20r%3D%22.8%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2316223a%22%2F%3E%3Cstop%20offset%3D%22.6%22%20stop-color%3D%22%230a0f1c%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23060912%22%2F%3E%3C%2FradialGradient%3E%3C%2Fdefs%3E%3Crect%20x%3D%22-32%22%20y%3D%22-32%22%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2215%22%20fill%3D%22url(%23bg)%22%2F%3E%3Cg%20transform%3D%22scale(1.22)%22%3E%0A%3Ccircle%20r%3D%2221%22%20fill%3D%22url(%23glow)%22%2F%3E%0A%3Ccircle%20r%3D%2218%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224%22%20opacity%3D%22.38%22%2F%3E%0A%3Cpath%20d%3D%22M0%2C-18%20A18%2C18%200%200%2C1%2017.4%2C4.6%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224.4%22%20stroke-linecap%3D%22round%22%2F%3E%0A%3Cpath%20d%3D%22M-9.5%2C-7%20L0%2C10%20L9.5%2C-7%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%224.8%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3Ccircle%20cx%3D%2217.4%22%20cy%3D%224.6%22%20r%3D%223.2%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E">
++<style id="ov-critical">html{background:#0a0f1c;color:#e6edf7;color-scheme:dark}</style><script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>
++<link rel="manifest" href="/manifest.webmanifest">
++<link rel="alternate" type="text/html" hreflang="en" href="https://ai.openvibe.network/">
++<meta property="og:type" content="website">
++<meta property="og:site_name" content="ai.openvibe.network">
++<meta property="og:url" content="https://ai.openvibe.network/">
++<meta property="og:title" content="ai.openvibe.network — Moved: OpenVibe.AI&#39;s public home is ai.openvibe.services.">
++<meta property="og:description" content="OpenVibe.AI&#39;s public address is now ai.openvibe.services, next to the rest of the network&#39;s programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.">
++<meta property="og:image" content="https://openvibe.network/assets/logo-512.png">
++<meta property="og:image:alt" content="OpenVibe">
++<meta name="twitter:card" content="summary">
++<meta name="twitter:title" content="ai.openvibe.network — Moved: OpenVibe.AI&#39;s public home is ai.openvibe.services.">
++<meta name="twitter:description" content="OpenVibe.AI&#39;s public address is now ai.openvibe.services, next to the rest of the network&#39;s programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.">
++<meta name="twitter:image" content="https://openvibe.network/assets/logo-512.png">
++<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://ai.openvibe.network/#site","url":"https://ai.openvibe.network/","name":"ai.openvibe.network","description":"OpenVibe.AI's public address is now ai.openvibe.services, next to the rest of the network's programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.","inLanguage":"en","publisher":{"@type":"Organization","@id":"https://openvibe.network/#org","name":"OpenVibe","url":"https://openvibe.network/","logo":"https://openvibe.network/assets/logo-512.png","sameAs":["https://github.com/OpenVibers","https://discord.gg/M6MuRUaeJj"]},"isPartOf":{"@type":"WebSite","@id":"https://openvibe.network/#site","name":"OpenVibe.Network","url":"https://openvibe.network/"}},{"@type":"WebPage","@id":"https://ai.openvibe.network/#page","url":"https://ai.openvibe.network/","name":"ai.openvibe.network — Moved: OpenVibe.AI's public home is ai.openvibe.services.","description":"OpenVibe.AI's public address is now ai.openvibe.services, next to the rest of the network's programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.","isPartOf":{"@id":"https://ai.openvibe.network/#site"},"datePublished":"2026-09-17","dateModified":"2026-10-03","breadcrumb":{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"OpenVibe","item":"https://openvibe.network/"},{"@type":"ListItem","position":2,"name":"ai.openvibe.network","item":"https://ai.openvibe.network/"}]}}]}</script>
++<script>(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script>
++<script src="/shared/theme-loader.js?v=ee4a6754d0f7" defer></script>
++<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
++<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" media="print" onload="this.media='all'">
++<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"></noscript>
++<style>
++*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
++:root{--bg-primary:#0a0f1c;--bg-secondary:#101828;--bg-card:#131c2e;--bg-hover:#1c2a44;--border:#1f2d47;--text-primary:#e6edf7;--text-secondary:#96a7c2;--text-muted:#7386a3;--accent:#3b82f6;--accent-light:#60a5fa;--accent-rgb:59,130,246;--on-accent:#fff;--site:#c084fc;--site-rgb:192,132,252}
++html{scroll-behavior:smooth}
++body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden}
++main{flex:1;position:relative}
++.orbs{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
++.orbs i{position:absolute;border-radius:50%;filter:blur(90px);opacity:.16;animation:drift 26s ease-in-out infinite alternate}
++.orbs i:nth-child(1){width:420px;height:420px;background:var(--site);top:-140px;left:-120px}
++.orbs i:nth-child(2){width:360px;height:360px;background:var(--accent);top:40%;right:-140px;animation-delay:-9s}
++.orbs i:nth-child(3){width:300px;height:300px;background:var(--site);bottom:-120px;left:38%;animation-delay:-17s}
++@keyframes drift{0%{transform:translate(0,0) scale(1)}50%{transform:translate(-26px,36px) scale(1.07)}100%{transform:translate(34px,-28px) scale(.95)}}
++.wrap{position:relative;z-index:1;max-width:1040px;margin:0 auto;padding:0 20px}
++.hero{padding:84px 0 44px;text-align:center}
++.hero .mark{display:inline-grid;place-items:center;width:84px;height:84px;border-radius:24px;background:rgba(var(--site-rgb),.12);border:1px solid rgba(var(--site-rgb),.35);color:var(--site);font-size:34px;margin-bottom:22px;box-shadow:0 20px 60px rgba(var(--site-rgb),.18);animation:rise .6s cubic-bezier(.2,.8,.2,1) both}
++.hero .badge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-size:12px;font-weight:600;color:var(--text-secondary);letter-spacing:.3px;margin-bottom:22px;animation:rise .6s .05s cubic-bezier(.2,.8,.2,1) both}
++.hero .badge b{color:var(--site)}
++.hero h1{font-size:clamp(34px,6vw,58px);font-weight:800;letter-spacing:-1.4px;line-height:1.08;animation:rise .6s .1s cubic-bezier(.2,.8,.2,1) both}
++.hero h1 .d{color:var(--site)}
++.hero h1 .tld{color:var(--text-secondary)}
++.hero .tag{font-size:clamp(18px,2.6vw,24px);font-weight:600;color:var(--text-primary);margin:18px 0 10px;animation:rise .6s .16s cubic-bezier(.2,.8,.2,1) both}
++.hero p.lead{color:var(--text-secondary);font-size:16px;line-height:1.65;max-width:680px;margin:0 auto;animation:rise .6s .22s cubic-bezier(.2,.8,.2,1) both}
++.ctas{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:30px;animation:rise .6s .3s cubic-bezier(.2,.8,.2,1) both}
++.hero .status{margin:22px auto 0;max-width:720px;font-size:12.5px;line-height:1.7;color:var(--text-muted);border:1px dashed var(--border);border-radius:12px;padding:10px 14px;background:rgba(0,0,0,.15)}
++.hero .status b{color:var(--site)}
++.hero .status code{font-size:12px;color:var(--text-secondary);background:var(--bg-secondary);padding:1px 6px;border-radius:6px}
++.hero .status a{color:var(--text-secondary)}
++.btn{display:inline-flex;align-items:center;gap:9px;padding:12px 20px;border-radius:12px;font-size:14px;font-weight:600;text-decoration:none;border:1px solid var(--border);color:var(--text-primary);background:var(--bg-secondary);transition:transform .15s,border-color .15s,box-shadow .15s}
++.btn:hover{transform:translateY(-1px);border-color:var(--site);box-shadow:0 10px 30px rgba(var(--site-rgb),.15)}
++.btn-primary{background:var(--site);border-color:var(--site);color:#0b0d10}
++.btn-primary:hover{filter:brightness(1.08)}
++@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
++section{padding:36px 0}
++h2{font-size:22px;font-weight:700;letter-spacing:-.3px;margin-bottom:6px}
++.lead2{color:var(--text-secondary);font-size:14px;margin-bottom:20px}
++.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
++.card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px;transition:transform .2s,border-color .2s,box-shadow .2s;text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:8px}
++a.card:hover{transform:translateY(-3px);border-color:rgba(var(--site-rgb),.6);box-shadow:0 16px 40px rgba(0,0,0,.35)}
++.card .ic{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;background:rgba(var(--site-rgb),.12);color:var(--site);font-size:17px}
++.card b{font-size:15px}
++.card span{font-size:13px;color:var(--text-secondary);line-height:1.55}
++.card .go{margin-top:auto;font-size:12.5px;font-weight:600;color:var(--site)}
++.highlight .hl-card{border:1px solid rgba(var(--accent-rgb),.45);background:rgba(var(--accent-rgb),.10);border-radius:18px;padding:22px 22px 18px}
++.highlight h2{margin:0 0 8px}.highlight h2 i{color:var(--accent-light)}.highlight p{margin:0 0 10px}
++.highlight ul{margin:0 0 14px;padding-left:20px}.highlight li{margin:4px 0}
++.faq details{border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px 14px;margin:8px 0}
++.faq summary{cursor:pointer;font-weight:600}.faq details p{margin:8px 0 0;opacity:.85}
++.meanwhile .card .ic{background:rgba(var(--accent-rgb),.12);color:var(--accent-light)}
++.net .card .ic{background:var(--bg-hover);color:var(--text-primary)}
++.rooms{display:flex;flex-wrap:wrap;gap:8px}
++.rooms a{font-size:12.5px;padding:7px 12px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary);text-decoration:none;transition:all .15s;background:var(--bg-secondary)}
++.rooms a:hover{color:var(--text-primary);border-color:var(--site)}
++.rooms a i{margin-right:6px;opacity:.8}
++.strip{border:1px solid var(--border);border-radius:18px;background:var(--bg-secondary);padding:30px 26px;text-align:center;margin:10px 0 50px}
++.strip h3{font-size:19px;font-weight:700;margin:12px 0 8px}
++.strip p{font-size:14px;color:var(--text-secondary);max-width:600px;margin:0 auto 18px;line-height:1.6}
++.strip .ov-mark{width:44px;height:44px}
++.hero.split{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:40px;align-items:center;text-align:left;padding-top:64px}
++.hero.split .ctas{justify-content:flex-start}.hero.split p.lead{margin:0}
++.viz{background:var(--bg-card);border:1px solid var(--border);border-radius:20px;padding:14px;box-shadow:0 30px 80px rgba(0,0,0,.45),0 0 0 1px rgba(var(--site-rgb),.08);animation:rise .7s .25s cubic-bezier(.2,.8,.2,1) both;text-align:left}
++.viz figcaption{margin-top:10px;font-size:11.5px;color:var(--text-muted);text-align:center}
++.viz-top{display:flex;align-items:center;gap:8px;font-size:13px;padding:2px 4px 12px}
++.viz-top b{font-weight:700}.viz-top .muted{color:var(--text-muted)}
++.viz-top .lat{margin-left:auto;font-size:12px;color:var(--site);background:rgba(var(--site-rgb),.1);border:1px solid rgba(var(--site-rgb),.3);padding:3px 9px;border-radius:999px}
++.viz-top .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;box-shadow:0 0 0 4px rgba(34,197,94,.18)}
++.viz-top .av{width:26px;height:26px;border-radius:8px;display:grid;place-items:center;background:rgba(var(--site-rgb),.15);color:var(--site);font-size:13px}
++.viz-video{position:relative;aspect-ratio:16/9;border-radius:14px;overflow:hidden;background:radial-gradient(120% 90% at 30% 20%,rgba(var(--site-rgb),.28),transparent 60%),linear-gradient(160deg,#1a2338,#0b1020 70%);border:1px solid var(--border)}
++.viz-video::after{content:'';position:absolute;left:10%;right:10%;bottom:0;height:42%;background:linear-gradient(transparent,rgba(0,0,0,.35)),repeating-linear-gradient(90deg,rgba(255,255,255,.05) 0 1px,transparent 1px 38px);transform:perspective(220px) rotateX(58deg);transform-origin:bottom}
++.viz-video .scan{position:absolute;inset:0;background:linear-gradient(transparent 0,rgba(255,255,255,.04) 50%,transparent 100%);background-size:100% 6px;opacity:.5}
++.viz-video .onair{position:absolute;top:10px;left:10px;font-size:11px;font-weight:700;padding:3px 8px;border-radius:6px;background:rgba(239,68,68,.9);color:#fff}.viz-video .onair i{font-size:7px;vertical-align:2px;margin-right:3px}
++.viz-video .cam{position:absolute;bottom:10px;right:10px;font-size:11px;color:#dbe4f5;background:rgba(0,0,0,.45);padding:3px 8px;border-radius:6px}
++.viz-ctrl{display:flex;gap:14px;align-items:center;margin-top:12px}
++.dpad{display:grid;grid-template-columns:repeat(3,34px);grid-template-rows:repeat(3,34px);gap:4px;flex:none}
++.dpad span{display:grid;place-items:center;border-radius:9px;background:var(--bg-hover);color:var(--text-primary);font-size:15px;border:1px solid var(--border)}
++.dpad .u{grid-area:1/2}.dpad .l{grid-area:2/1}.dpad .c{grid-area:2/2;background:rgba(var(--site-rgb),.15);border-color:rgba(var(--site-rgb),.35)}.dpad .r{grid-area:2/3}.dpad .d{grid-area:3/2}
++.dpad .u{background:rgba(var(--site-rgb),.22);border-color:rgba(var(--site-rgb),.5)}
++.viz-side{flex:1;min-width:0;display:grid;gap:8px}
++.viz-side .keys{display:flex;flex-wrap:wrap;gap:6px}
++.viz-side .keys span{font-size:11.5px;padding:5px 9px;border-radius:8px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
++.viz-side .meter{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--text-muted)}
++.viz-side .meter i{flex:1;height:6px;border-radius:9px;background:linear-gradient(90deg,#22c55e var(--v),var(--bg-hover) var(--v))}
++.viz-side .meter b{color:var(--text-secondary);font-weight:600}
++.viz-side .estop{justify-self:end;font-size:12px;font-weight:800;letter-spacing:.08em;color:#fff;background:#dc2626;border-radius:10px;padding:7px 16px;box-shadow:0 0 0 3px rgba(220,38,38,.25)}
++.feed{list-style:none;display:grid;gap:8px;padding:2px}
++.feed li{display:flex;align-items:flex-start;gap:10px;font-size:13.5px;line-height:1.45;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
++.feed li i{color:#22c55e;margin-top:3px}
++.feed li.ask{flex-wrap:wrap;border-color:rgba(var(--site-rgb),.45);background:rgba(var(--site-rgb),.08);color:var(--text-primary)}.feed li.ask i{color:var(--site)}.feed li.ask>span:first-of-type{flex:1;min-width:160px}
++.feed .pills{display:flex;gap:6px;margin-left:auto}.feed .pills span{font-size:12px;font-weight:600;padding:4px 11px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}.feed .pills .ok{background:var(--site);border-color:var(--site);color:#0b0d10}
++.steps{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;counter-reset:s}
++.steps li{position:relative;background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px 20px 20px;display:flex;flex-direction:column;gap:8px}
++.steps .n{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:14px;color:#0b0d10;background:var(--site)}
++.steps b{font-size:15px}.steps span:last-child{font-size:13px;color:var(--text-secondary);line-height:1.55}
++.grid.three{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
++.viz .task{font-size:14px;line-height:1.5;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary)}
++.viz .modes{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
++.viz .modes span{font-size:11.5px;font-weight:600;padding:4px 10px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}
++.viz .modes .on{background:var(--site);border-color:var(--site);color:#0b0d10}
++.cands{list-style:none;display:grid;gap:6px}
++.cands li{display:grid;grid-template-columns:18px minmax(0,1fr) 54px 50px 44px;align-items:center;gap:8px;font-size:12.5px;padding:8px 10px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary);position:relative}
++.cands li i{color:var(--text-muted)}.cands li b{font-weight:600;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
++.cands li span{text-align:right;font-variant-numeric:tabular-nums}
++.cands li.pick{border-color:rgba(var(--site-rgb),.55);background:rgba(var(--site-rgb),.09)}.cands li.pick i{color:var(--site)}
++.cands li em{position:absolute;top:-8px;right:10px;font-style:normal;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 7px;border-radius:999px;background:var(--site);color:#0b0d10}
++.viz .why{margin-top:10px;font-size:12.5px;line-height:1.5;color:var(--text-secondary)}.viz .why i{color:var(--site);margin-right:4px}
++.choice{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
++.card.big{padding:24px}.card.big b{font-size:17px}
++.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px}
++.chips span{font-size:12.5px;padding:6px 11px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-secondary)}
++.chips.big span{font-size:13.5px;padding:9px 14px;color:var(--text-primary)}.chips.big span i{color:var(--site);margin-right:8px}
++.build .card{border-style:dashed;background:rgba(0,0,0,.15)}.build .card b i{color:var(--site);margin-right:6px}.build .card span a{color:var(--text-secondary)}.build .card code{font-size:12px}
++@media (max-width:860px){.hero.split{grid-template-columns:1fr;text-align:center;gap:28px}.hero.split .ctas{justify-content:center}.hero.split p.lead{margin:0 auto}}
++@media (max-width:600px){.hero{padding-top:56px}.ctas .btn{width:100%;justify-content:center}.viz-ctrl{flex-direction:column;align-items:stretch}.dpad{align-self:center}.cands li{grid-template-columns:14px minmax(0,1fr) 30px 40px 30px;gap:6px;font-size:12px;padding:8px}}
++@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
++</style>
++</head>
++<body>
++<div class="orbs"><i></i><i></i><i></i></div>
++<div id="navbar-mount"></div>
++<main class="wrap">
++ <header class="hero">
++ <div class="mark"><i class="fa-solid fa-robot" aria-hidden="true"></i></div>
++ <div class="badge"><span class="ov-mark" data-size="16" data-variant="ai"></span> Part of the OpenVibe network · <b>in development</b></div>
++ <h1><span class="d">ai</span><span class="tld">.openvibe.network</span></h1>
++ <div class="tag">Moved: OpenVibe.AI&#39;s public home is ai.openvibe.services.</div>
++ <p class="lead">OpenVibe.AI&#39;s public address is now ai.openvibe.services, next to the rest of the network&#39;s programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.</p>
++ <div class="ctas">
++ <a class="btn btn-primary" href="https://ai.openvibe.services/"><i class="fa-solid fa-arrow-right"></i> ai.openvibe.services</a>
++ <a class="btn" href="https://openvibe.network/#network"><i class="fa-solid fa-circle-nodes"></i> The whole network</a>
++ <a class="btn" href="https://discord.gg/M6MuRUaeJj" rel="noopener"><i class="fa-brands fa-discord"></i> Follow the build</a>
++ </div>
++ <p class="status"><i class="fa-solid fa-clock" aria-hidden="true"></i> Status: <b>in development</b> · <a href="https://github.com/OpenVibers/OpenVibe.AI" rel="noopener"><code>OpenVibers/OpenVibe.AI</code></a> moved to <a href="https://ai.openvibe.services/">ai.openvibe.services</a>; this address serves only this notice. · as of 2026-09-29 · <a href="/status.json">status.json</a></p>
++ </header>
++
++
++ <section id="what" class="meanwhile">
++ <h2>Where to go</h2>
++ <p class="lead2">OpenVibe.AI lives on OpenVibe.Services now.</p>
++ <div class="grid">
++ <a class="card" href="https://ai.openvibe.services/"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>ai.openvibe.services</b><span>OpenVibe.AI: the gateway, templates, workflows and runs.</span><span class="go">ai.openvibe.services →</span></a>
++ <a class="card" href="https://openvibe.services/"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>OpenVibe.Services</b><span>Every programmable OpenVibe service in one place.</span><span class="go">openvibe.services →</span></a>
++ </div>
++ </section>
++
++ <section class="net">
++ <h2>The OpenVibe network</h2>
++ <p class="lead2">One account for all of it. Sign in once and every site signs you in.</p>
++ <div class="grid">
++ <a class="card" href="https://openvibe.live"><div class="ic"><i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i></div><b>OpenVibe.Live</b><span>Go live, chat, clip, restream</span><span class="go">openvibe.live →</span></a>
++ <a class="card" href="https://openvibe.tools"><div class="ic"><i class="fa-solid fa-screwdriver-wrench" aria-hidden="true"></i></div><b>OpenVibe.Tools</b><span>Hundreds of online tools</span><span class="go">openvibe.tools →</span></a>
++ <a class="card" href="https://openvibe.community"><div class="ic"><i class="fa-solid fa-people-group" aria-hidden="true"></i></div><b>OpenVibe.Community</b><span>Pastes, posts, people</span><span class="go">openvibe.community →</span></a>
++ <a class="card" href="https://openvibe.games"><div class="ic"><i class="fa-solid fa-gamepad" aria-hidden="true"></i></div><b>OpenVibe.Games</b><span>Scraplandia and browser games</span><span class="go">openvibe.games →</span></a>
++ <a class="card" href="https://openvibe.media"><div class="ic"><i class="fa-solid fa-photo-film" aria-hidden="true"></i></div><b>OpenVibe.Media</b><span>VODs, clips and files</span><span class="go">openvibe.media →</span></a>
++ <a class="card" href="https://openvibe.network"><div class="ic"><i class="fa-solid fa-circle-nodes" aria-hidden="true"></i></div><b>OpenVibe.Network</b><span>One account, every site</span><span class="go">openvibe.network →</span></a>
++ </div>
++ <h2 style="margin-top:28px;font-size:16px">Other OpenVibe domains, not open yet</h2>
++ <div class="rooms" style="margin-top:8px">
++ <a href="https://openvibe.news/"><i class="fa-solid fa-newspaper" aria-hidden="true"></i>OpenVibe.News</a>
++ <a href="https://openvibe.reviews/"><i class="fa-solid fa-star-half-stroke" aria-hidden="true"></i>OpenVibe.Reviews</a>
++ <a href="https://openvibe.tips/"><i class="fa-solid fa-hand-holding-heart" aria-hidden="true"></i>OpenVibe.Tips</a>
++ <a href="https://openvibe.vip/"><i class="fa-solid fa-crown" aria-hidden="true"></i>OpenVibe.VIP</a>
++ <a href="https://openvibe.trade/"><i class="fa-solid fa-chart-line" aria-hidden="true"></i>OpenVibe.Trade</a>
++ <a href="https://openvibe.host/"><i class="fa-solid fa-server" aria-hidden="true"></i>OpenVibe.Host</a>
++ <a href="https://openvibe.deals/"><i class="fa-solid fa-tags" aria-hidden="true"></i>OpenVibe.Deals</a>
++ <a href="https://openvibe.coupons/"><i class="fa-solid fa-ticket" aria-hidden="true"></i>OpenVibe.Coupons</a>
++ <a href="https://openre.stream/"><i class="fa-solid fa-satellite-dish" aria-hidden="true"></i>OpenRe.Stream</a>
++ <a href="https://auth.openvibe.network/"><i class="fa-solid fa-key" aria-hidden="true"></i>auth.openvibe.network</a>
++ <a href="https://api.openvibe.network/"><i class="fa-solid fa-plug" aria-hidden="true"></i>api.openvibe.network</a>
++ <a href="https://admin.openvibe.network/"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i>admin.openvibe.network</a>
++ <a href="https://themes.openvibe.network/"><i class="fa-solid fa-palette" aria-hidden="true"></i>themes.openvibe.network</a>
++ <a href="https://ai.openvibe.services/"><i class="fa-solid fa-robot" aria-hidden="true"></i>ai.openvibe.services</a>
++ <a href="https://openvibe.pics/"><i class="fa-solid fa-images" aria-hidden="true"></i>OpenVibe.Pics</a>
++ <a href="https://openvibe.download/"><i class="fa-solid fa-cloud-arrow-down" aria-hidden="true"></i>OpenVibe.Download</a>
++ <a href="https://openvibe.space/"><i class="fa-solid fa-layer-group" aria-hidden="true"></i>OpenVibe.Space</a>
++ <a href="https://openvibe.food/"><i class="fa-solid fa-utensils" aria-hidden="true"></i>OpenVibe.Food</a>
++ <a href="https://openvibe.quest/"><i class="fa-solid fa-map" aria-hidden="true"></i>OpenVibe.Quest</a>
++ <a href="https://openvibe.rent/"><i class="fa-solid fa-key" aria-hidden="true"></i>OpenVibe.Rent</a>
++ <a href="https://openvibe.homes/"><i class="fa-solid fa-house" aria-hidden="true"></i>OpenVibe.Homes</a>
++ <a href="https://openvibe.services/"><i class="fa-solid fa-server" aria-hidden="true"></i>OpenVibe.Services</a>
++ <a href="https://openvibe.run/"><i class="fa-solid fa-terminal" aria-hidden="true"></i>OpenVibe.Run</a>
++ <a href="https://openvibe.work/"><i class="fa-solid fa-briefcase" aria-hidden="true"></i>OpenVibe.Work</a>
++ <a href="https://openvibe.zone/"><i class="fa-solid fa-box-archive" aria-hidden="true"></i>OpenVibe.Zone</a>
++ <a href="https://openvibe.video/"><i class="fa-solid fa-film" aria-hidden="true"></i>OpenVibe.Video</a>
++ <a href="https://openvibe.website/"><i class="fa-solid fa-globe" aria-hidden="true"></i>OpenVibe.Website</a>
++ <a href="https://openvibe.help/"><i class="fa-solid fa-life-ring" aria-hidden="true"></i>OpenVibe.Help</a>
++ <a href="https://openvibe.bot/"><i class="fa-solid fa-robot" aria-hidden="true"></i>OpenVibe.Bot</a>
++ <a href="https://openvibe.watch/"><i class="fa-solid fa-eye" aria-hidden="true"></i>OpenVibe.Watch</a>
++ <a href="https://openvibe.actor/"><i class="fa-solid fa-user-astronaut" aria-hidden="true"></i>OpenVibe.Actor</a>
++ <a href="https://openvibe.events/"><i class="fa-solid fa-bolt" aria-hidden="true"></i>OpenVibe.Events</a>
++ </div>
++ </section>
++
++ <div class="strip">
++ <span class="ov-mark" data-size="44" data-variant="ai"></span>
++ <h3>Open source. Community run.</h3>
++ <p>Every OpenVibe site is built in the open by the people using it — free speech within the rules, no gatekeeping, one identity that follows you everywhere.</p>
++ <a class="btn" href="https://github.com/OpenVibers" rel="noopener"><i class="fa-brands fa-github"></i> OpenVibers on GitHub</a>
++ </div>
++</main>
++<style id="ovf-css">
++.ovf{background:var(--bg-secondary,#12131c);border-top:1px solid var(--border,rgba(255,255,255,.08));margin-top:40px;
++ padding:clamp(24px,4vw,40px) clamp(14px,4vw,32px) calc(clamp(20px,3vw,28px) + env(safe-area-inset-bottom,0px));}
++.ovf[data-variant="compact"]{margin-top:24px;padding:14px clamp(12px,3vw,24px) calc(14px + env(safe-area-inset-bottom,0px));}
++.ovf-inner{max-width:1240px;margin:0 auto}
++.ovf-inner--compact{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px 18px;text-align:center}
++.ovf-cols{display:grid;gap:clamp(20px,3vw,36px);grid-template-columns:minmax(230px,1.6fr) repeat(auto-fit,minmax(120px,1fr));align-items:start}
++.ovf-col{display:flex;flex-direction:column;gap:8px;min-width:0}
++.ovf-col h3{margin:0 0 2px;font-size:.72rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted,#8b93ad)}
++.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}
++.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
++.ovf-more{font-weight:600;opacity:.85}.ovf-ic{flex:none;vertical-align:-.14em}.ovf-more .ovf-ic{font-size:.85em;margin-left:3px;transition:transform .15s}.ovf-more:hover .ovf-ic{transform:translateX(3px)}.ovf-social .ovf-ic{font-size:17px}.ovf-account-links .ovf-ic{font-size:1.05em;opacity:.85}
++.ovf-brand{display:inline-flex;align-items:center;gap:9px;color:var(--text-primary,#f1f4fb);font-size:1.05rem;font-weight:700;text-decoration:none}
++.ovf-brand--sm{font-size:.9rem}
++.ovf-tagline{margin:0;font-size:.82rem;line-height:1.55;color:var(--text-muted,#8b93ad);max-width:46ch}
++.ovf-social{display:flex;gap:8px;margin-top:4px}
++.ovf-social a{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--text-secondary,#b6bdd2);
++ background:var(--bg-tertiary,#1a1b28);border:1px solid var(--border,rgba(255,255,255,.08));font-size:.95rem;transition:color .15s,border-color .15s,transform .15s}
++.ovf-social a:hover{color:var(--accent,#60a5fa);border-color:var(--accent,#60a5fa);transform:translateY(-2px)}
++.ovf-account{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 18px;
++ margin-top:clamp(18px,2.5vw,26px);padding:10px 14px;border-radius:14px;
++ background:color-mix(in srgb, var(--accent,#60a5fa) 8%, var(--bg-tertiary,#1a1b28));
++ border:1px solid color-mix(in srgb, var(--accent,#60a5fa) 26%, var(--border,rgba(255,255,255,.08)));
++ animation:ovfIn .4s ease-out both}
++@keyframes ovfIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
++.ovf-account-who{display:inline-flex;align-items:center;gap:8px;font-size:.84rem;color:var(--text-secondary,#b6bdd2)}
++.ovf-account-who strong{color:var(--text-primary,#f1f4fb)}
++.ovf-avatar{width:24px;height:24px;border-radius:50%;overflow:hidden;display:grid;place-items:center;
++ background:var(--accent,#60a5fa);color:#fff;font-size:.7rem;font-weight:800;flex:none}
++.ovf-avatar img{width:100%;height:100%;object-fit:cover}
++.ovf-account-links{display:flex;flex-wrap:wrap;gap:6px}
++.ovf-account-links a{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;font-size:.78rem;font-weight:600;
++ color:var(--text-secondary,#b6bdd2);background:var(--bg-secondary,#12131c);border:1px solid var(--border,rgba(255,255,255,.08));
++ text-decoration:none;transition:color .15s,border-color .15s,transform .15s}
++.ovf-account-links a:hover{color:var(--accent,#60a5fa);border-color:var(--accent,#60a5fa);transform:translateY(-1px)}
++.ovf-account-links i{font-size:.72rem;opacity:.8}
++.ovf-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;
++ margin-top:clamp(20px,3vw,30px);padding-top:16px;border-top:1px solid var(--border,rgba(255,255,255,.08));
++ padding-inline:clamp(0px, calc(84px - (100vw - 1240px) / 2), 84px)}
++.ovf-copy{color:var(--text-muted,#8b93ad);font-size:.78rem}
++.ovf-legal-inline,.ovf-compact-links{display:flex;flex-wrap:wrap;gap:14px}
++.ovf [hidden],.ovf-account[hidden]{display:none!important}
++.ovf .ovf-shipped.ov-shipped-latest{font-size:.76rem;padding:4px 12px;max-width:min(520px,100%);background:transparent}
++.ovf-legal-inline a,.ovf-compact-links a{color:var(--text-muted,#8b93ad);font-size:.78rem;text-decoration:none;transition:color .15s}
++.ovf-legal-inline a:hover,.ovf-compact-links a:hover{color:var(--accent,#60a5fa)}
++.ovf-compact-meta{color:var(--text-muted,#8b93ad);font-size:.74rem}
++@media (max-width:900px){.ovf-cols{grid-template-columns:repeat(2,minmax(0,1fr))}.ovf-col--brand{grid-column:1/-1}}
++@media (max-width:560px){
++ .ovf{padding-bottom:calc(20px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
++ .ovf[data-variant="compact"]{padding-bottom:calc(14px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
++ .ovf-cols{gap:22px 16px}
++ .ovf-bar{justify-content:center;text-align:center;padding-inline:0}
++ .ovf-legal-inline{justify-content:center}
++ .ovf-account{flex-direction:column;align-items:flex-start}
++}
++@media (prefers-reduced-motion:reduce){.ovf-account{animation:none}.ovf-col a:hover,.ovf-social a:hover,.ovf-account-links a:hover{transform:none}}
++.ovf{-webkit-tap-highlight-color:transparent}
++:where(.ovf) :where(a,button):active{scale:.97}
++:where(.ovf) :where(a,button):focus-visible{outline:2px solid var(--accent-light,var(--accent,#3b82f6));outline-offset:2px;border-radius:4px}
++@media (prefers-reduced-motion:reduce){:where(.ovf) :where(a,button):active{scale:none;opacity:.82}}</style><footer id="ov-footer" class="ovf" data-variant="full"><div class="ovf-inner">
++ <div class="ovf-cols">
++ <div class="ovf-col ovf-col--brand">
++ <a class="ovf-brand" href="/"><span class="ov-mark" data-size="26"></span><span>OpenVibe</span></a>
++ <p class="ovf-tagline">Open source and community run. Built in the open by the people using it.</p>
++ <div class="ovf-social">
++ <a href="https://github.com/OpenVibers" target="_blank" rel="noopener" aria-label="GitHub" title="GitHub"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path fill="currentColor" stroke="none" d="M12 2.2a9.8 9.8 0 0 0-3.1 19.1c.5.1.7-.2.7-.5v-1.7c-2.7.6-3.3-1.2-3.3-1.2-.4-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.2-.2-4.5-1.1-4.5-4.8 0-1.1.4-1.9 1-2.6-.1-.2-.4-1.2.1-2.6 0 0 .8-.3 2.7 1a9.3 9.3 0 0 1 4.9 0c1.9-1.3 2.7-1 2.7-1 .5 1.4.2 2.4.1 2.6.6.7 1 1.5 1 2.6 0 3.7-2.3 4.6-4.5 4.8.4.3.7.9.7 1.800v2.700c0 .3.2.6.7.5A9.8 9.8 0 0 0 12 2.200z"/></svg></a><a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener" aria-label="Discord" title="Discord"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path fill="currentColor" stroke="none" d="M19.3 5.300a16.5 16.5 0 0 0-4.1-1.300l-.5 1a15.3 15.3 0 0 0-5.4 0l-.5-1a16.5 16.5 0 0 0-4.1 1.300C2.1 9.2 1.4 13 1.7 16.700a16.6 16.6 0 0 0 5.1 2.600l1.1-1.800c-.6-.2-1.2-.5-1.7-.8l.4-.3a11.8 11.8 0 0 0 10.8 0l.4.3c-.5.3-1.1.6-1.7.8l1.1 1.800a16.6 16.6 0 0 0 5.1-2.600c.4-4.3-.7-8.1-3-11.400zM8.7 14.400c-1 0-1.8-.9-1.8-2.100s.8-2.1 1.8-2.1 1.9 1 1.8 2.100c0 1.2-.8 2.1-1.8 2.100zm6.6 0c-1 0-1.8-.9-1.8-2.100s.8-2.1 1.8-2.1 1.9 1 1.8 2.100c0 1.2-.8 2.1-1.8 2.100z"/></svg></a>
++ <a href="/sitemap.xml" aria-label="Sitemap" title="Sitemap"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="3.5" width="6" height="4.5" rx="1"/><rect x="3" y="16" width="6" height="4.5" rx="1"/><rect x="15" y="16" width="6" height="4.5" rx="1"/><path d="M12 8v4M6 16v-2.500h12V16"/></svg></a>
++ </div>
++ </div>
++
++ <nav class="ovf-col" aria-label="The OpenVibe network">
++ <h3>Network</h3>
++ <a href="https://openvibe.live" title="Go live, restream and clip">Live streaming</a><a href="https://openvibe.tools" title="The whole toolbox">Online tools</a><a href="https://dev.openvibe.tools" title="Formatters, encoders, validators">Developer tools</a><a href="https://net.openvibe.tools" title="Ping, DNS, Whois, SSL">Network diagnostics</a><a href="https://img.openvibe.tools" title="Convert, resize, compress">Image converter</a><a href="https://audio.openvibe.tools" title="Convert, trim, pitch-shift">Audio converter</a>
++ <a class="ovf-more" href="https://openvibe.network" target="_blank" rel="noopener">Everything else <svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
++ </nav>
++
++ <nav class="ovf-col" aria-label="Legal">
++ <h3>Legal</h3>
++ <a href="https://openvibe.live/tos">Terms of Service</a><a href="https://openvibe.live/privacy">Privacy Policy</a><a href="https://openvibe.live/dmca">DMCA</a>
++ </nav>
++ </div>
++ <div class="ovf-account" id="ovf-account" hidden></div>
++ <div class="ovf-bar">
++ <span class="ovf-copy">Open source &amp; community driven · built in the open</span>
++ <a class="ovf-shipped" data-ov-shipped="latest" data-service="ai" href="https://openvibe.network/updates?site=ai" hidden></a>
++ <span class="ovf-legal-inline">
++ <a href="https://openvibe.network/updates?site=ai">Updates</a>
++ <a href="https://openvibe.live/tos">Terms</a><a href="https://openvibe.live/privacy">Privacy</a><a href="https://openvibe.live/dmca">DMCA</a>
++ </span>
++ </div>
++ </div></footer>
++<script src="/shared/ov-mark.js?v=03f3596adcab" async></script>
++<script src="/shared/navbar.js?v=017a740747b4"></script>
++<script src="/shared/footer.js?v=a85ab893be88"></script>
++<script>
++(function () {
++ if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'ai', apiBase: 'https://openvibe.network', links: [{ label: 'Where to go', href: '#what' }, { label: 'The network', href: 'https://openvibe.network/#network', external: false }], history: { type: 'page', title: 'ai.openvibe.network' } }); } catch (e) {} }
++ if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: 'ai', variant: 'full', links: [{ heading: 'ai.openvibe.network', items: [{ name: 'Sign in', url: 'https://openvibe.network/login?return=https%3A%2F%2Fai.openvibe.network%2F' }, { name: 'The network', url: 'https://openvibe.network/#network' }, { name: 'Discord', url: 'https://discord.gg/M6MuRUaeJj' }] }] }); } catch (e) {} }
++})();
++</script>
++</body>
++</html>
ddiff --git a/notice-pages/realtime.openvibe.network.html b/notice-pages/realtime.openvibe.network.html
nnew file mode 100644
iindex 0000000..363478e
---- /dev/null
++++ b/notice-pages/realtime.openvibe.network.html
@@@ -0,0 +1,332 @@
++<!DOCTYPE html>
++<html lang="en">
++<head>
++<meta charset="utf-8">
++<meta name="viewport" content="width=device-width, initial-scale=1">
++<title>realtime.openvibe.network — Closed: realtime delivery is part of OpenVibe.Events.</title>
++<meta name="description" content="OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.">
++<meta name="keywords" content="openvibe realtime, openvibe events, server-sent events">
++<meta name="robots" content="noindex, follow">
++<meta name="theme-color" content="#34d399">
++<link rel="canonical" href="https://realtime.openvibe.network/">
++<link rel="icon" type="image/svg+xml" data-ov-icon="network" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-32%20-32%2064%2064%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22ring%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2360a5fa%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%232563eb%22%2F%3E%3C%2FlinearGradient%3E%0A%3CradialGradient%20id%3D%22glow%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2360a5fa%22%20stop-opacity%3D%22.34%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2360a5fa%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%0A%3CradialGradient%20id%3D%22bg%22%20cx%3D%22.5%22%20cy%3D%22.38%22%20r%3D%22.8%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2316223a%22%2F%3E%3Cstop%20offset%3D%22.6%22%20stop-color%3D%22%230a0f1c%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23060912%22%2F%3E%3C%2FradialGradient%3E%3C%2Fdefs%3E%3Crect%20x%3D%22-32%22%20y%3D%22-32%22%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2215%22%20fill%3D%22url(%23bg)%22%2F%3E%3Cg%20transform%3D%22scale(1.22)%22%3E%0A%3Ccircle%20r%3D%2221%22%20fill%3D%22url(%23glow)%22%2F%3E%0A%3Ccircle%20r%3D%2218%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224%22%20opacity%3D%22.38%22%2F%3E%0A%3Cpath%20d%3D%22M0%2C-18%20A18%2C18%200%200%2C1%2017.4%2C4.6%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224.4%22%20stroke-linecap%3D%22round%22%2F%3E%0A%3Cpath%20d%3D%22M-9.5%2C-7%20L0%2C10%20L9.5%2C-7%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%224.8%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3Ccircle%20cx%3D%2217.4%22%20cy%3D%224.6%22%20r%3D%223.2%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E">
++<style id="ov-critical">html{background:#0a0f1c;color:#e6edf7;color-scheme:dark}</style><script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>
++<link rel="manifest" href="/manifest.webmanifest">
++<link rel="alternate" type="text/html" hreflang="en" href="https://realtime.openvibe.network/">
++<meta property="og:type" content="website">
++<meta property="og:site_name" content="realtime.openvibe.network">
++<meta property="og:url" content="https://realtime.openvibe.network/">
++<meta property="og:title" content="realtime.openvibe.network — Closed: realtime delivery is part of OpenVibe.Events.">
++<meta property="og:description" content="OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.">
++<meta property="og:image" content="https://openvibe.network/assets/logo-512.png">
++<meta property="og:image:alt" content="OpenVibe">
++<meta name="twitter:card" content="summary">
++<meta name="twitter:title" content="realtime.openvibe.network — Closed: realtime delivery is part of OpenVibe.Events.">
++<meta name="twitter:description" content="OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.">
++<meta name="twitter:image" content="https://openvibe.network/assets/logo-512.png">
++<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://realtime.openvibe.network/#site","url":"https://realtime.openvibe.network/","name":"realtime.openvibe.network","description":"OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.","inLanguage":"en","publisher":{"@type":"Organization","@id":"https://openvibe.network/#org","name":"OpenVibe","url":"https://openvibe.network/","logo":"https://openvibe.network/assets/logo-512.png","sameAs":["https://github.com/OpenVibers","https://discord.gg/M6MuRUaeJj"]},"isPartOf":{"@type":"WebSite","@id":"https://openvibe.network/#site","name":"OpenVibe.Network","url":"https://openvibe.network/"}},{"@type":"WebPage","@id":"https://realtime.openvibe.network/#page","url":"https://realtime.openvibe.network/","name":"realtime.openvibe.network — Closed: realtime delivery is part of OpenVibe.Events.","description":"OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.","isPartOf":{"@id":"https://realtime.openvibe.network/#site"},"datePublished":"2026-09-17","dateModified":"2026-10-03","breadcrumb":{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"OpenVibe","item":"https://openvibe.network/"},{"@type":"ListItem","position":2,"name":"realtime.openvibe.network","item":"https://realtime.openvibe.network/"}]}}]}</script>
++<script>(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script>
++<script src="/shared/theme-loader.js?v=ee4a6754d0f7" defer></script>
++<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
++<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" media="print" onload="this.media='all'">
++<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"></noscript>
++<style>
++*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
++:root{--bg-primary:#0a0f1c;--bg-secondary:#101828;--bg-card:#131c2e;--bg-hover:#1c2a44;--border:#1f2d47;--text-primary:#e6edf7;--text-secondary:#96a7c2;--text-muted:#7386a3;--accent:#3b82f6;--accent-light:#60a5fa;--accent-rgb:59,130,246;--on-accent:#fff;--site:#34d399;--site-rgb:52,211,153}
++html{scroll-behavior:smooth}
++body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden}
++main{flex:1;position:relative}
++.orbs{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
++.orbs i{position:absolute;border-radius:50%;filter:blur(90px);opacity:.16;animation:drift 26s ease-in-out infinite alternate}
++.orbs i:nth-child(1){width:420px;height:420px;background:var(--site);top:-140px;left:-120px}
++.orbs i:nth-child(2){width:360px;height:360px;background:var(--accent);top:40%;right:-140px;animation-delay:-9s}
++.orbs i:nth-child(3){width:300px;height:300px;background:var(--site);bottom:-120px;left:38%;animation-delay:-17s}
++@keyframes drift{0%{transform:translate(0,0) scale(1)}50%{transform:translate(-26px,36px) scale(1.07)}100%{transform:translate(34px,-28px) scale(.95)}}
++.wrap{position:relative;z-index:1;max-width:1040px;margin:0 auto;padding:0 20px}
++.hero{padding:84px 0 44px;text-align:center}
++.hero .mark{display:inline-grid;place-items:center;width:84px;height:84px;border-radius:24px;background:rgba(var(--site-rgb),.12);border:1px solid rgba(var(--site-rgb),.35);color:var(--site);font-size:34px;margin-bottom:22px;box-shadow:0 20px 60px rgba(var(--site-rgb),.18);animation:rise .6s cubic-bezier(.2,.8,.2,1) both}
++.hero .badge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-size:12px;font-weight:600;color:var(--text-secondary);letter-spacing:.3px;margin-bottom:22px;animation:rise .6s .05s cubic-bezier(.2,.8,.2,1) both}
++.hero .badge b{color:var(--site)}
++.hero h1{font-size:clamp(34px,6vw,58px);font-weight:800;letter-spacing:-1.4px;line-height:1.08;animation:rise .6s .1s cubic-bezier(.2,.8,.2,1) both}
++.hero h1 .d{color:var(--site)}
++.hero h1 .tld{color:var(--text-secondary)}
++.hero .tag{font-size:clamp(18px,2.6vw,24px);font-weight:600;color:var(--text-primary);margin:18px 0 10px;animation:rise .6s .16s cubic-bezier(.2,.8,.2,1) both}
++.hero p.lead{color:var(--text-secondary);font-size:16px;line-height:1.65;max-width:680px;margin:0 auto;animation:rise .6s .22s cubic-bezier(.2,.8,.2,1) both}
++.ctas{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:30px;animation:rise .6s .3s cubic-bezier(.2,.8,.2,1) both}
++.hero .status{margin:22px auto 0;max-width:720px;font-size:12.5px;line-height:1.7;color:var(--text-muted);border:1px dashed var(--border);border-radius:12px;padding:10px 14px;background:rgba(0,0,0,.15)}
++.hero .status b{color:var(--site)}
++.hero .status code{font-size:12px;color:var(--text-secondary);background:var(--bg-secondary);padding:1px 6px;border-radius:6px}
++.hero .status a{color:var(--text-secondary)}
++.btn{display:inline-flex;align-items:center;gap:9px;padding:12px 20px;border-radius:12px;font-size:14px;font-weight:600;text-decoration:none;border:1px solid var(--border);color:var(--text-primary);background:var(--bg-secondary);transition:transform .15s,border-color .15s,box-shadow .15s}
++.btn:hover{transform:translateY(-1px);border-color:var(--site);box-shadow:0 10px 30px rgba(var(--site-rgb),.15)}
++.btn-primary{background:var(--site);border-color:var(--site);color:#0b0d10}
++.btn-primary:hover{filter:brightness(1.08)}
++@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
++section{padding:36px 0}
++h2{font-size:22px;font-weight:700;letter-spacing:-.3px;margin-bottom:6px}
++.lead2{color:var(--text-secondary);font-size:14px;margin-bottom:20px}
++.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
++.card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px;transition:transform .2s,border-color .2s,box-shadow .2s;text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:8px}
++a.card:hover{transform:translateY(-3px);border-color:rgba(var(--site-rgb),.6);box-shadow:0 16px 40px rgba(0,0,0,.35)}
++.card .ic{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;background:rgba(var(--site-rgb),.12);color:var(--site);font-size:17px}
++.card b{font-size:15px}
++.card span{font-size:13px;color:var(--text-secondary);line-height:1.55}
++.card .go{margin-top:auto;font-size:12.5px;font-weight:600;color:var(--site)}
++.highlight .hl-card{border:1px solid rgba(var(--accent-rgb),.45);background:rgba(var(--accent-rgb),.10);border-radius:18px;padding:22px 22px 18px}
++.highlight h2{margin:0 0 8px}.highlight h2 i{color:var(--accent-light)}.highlight p{margin:0 0 10px}
++.highlight ul{margin:0 0 14px;padding-left:20px}.highlight li{margin:4px 0}
++.faq details{border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px 14px;margin:8px 0}
++.faq summary{cursor:pointer;font-weight:600}.faq details p{margin:8px 0 0;opacity:.85}
++.meanwhile .card .ic{background:rgba(var(--accent-rgb),.12);color:var(--accent-light)}
++.net .card .ic{background:var(--bg-hover);color:var(--text-primary)}
++.rooms{display:flex;flex-wrap:wrap;gap:8px}
++.rooms a{font-size:12.5px;padding:7px 12px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary);text-decoration:none;transition:all .15s;background:var(--bg-secondary)}
++.rooms a:hover{color:var(--text-primary);border-color:var(--site)}
++.rooms a i{margin-right:6px;opacity:.8}
++.strip{border:1px solid var(--border);border-radius:18px;background:var(--bg-secondary);padding:30px 26px;text-align:center;margin:10px 0 50px}
++.strip h3{font-size:19px;font-weight:700;margin:12px 0 8px}
++.strip p{font-size:14px;color:var(--text-secondary);max-width:600px;margin:0 auto 18px;line-height:1.6}
++.strip .ov-mark{width:44px;height:44px}
++.hero.split{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:40px;align-items:center;text-align:left;padding-top:64px}
++.hero.split .ctas{justify-content:flex-start}.hero.split p.lead{margin:0}
++.viz{background:var(--bg-card);border:1px solid var(--border);border-radius:20px;padding:14px;box-shadow:0 30px 80px rgba(0,0,0,.45),0 0 0 1px rgba(var(--site-rgb),.08);animation:rise .7s .25s cubic-bezier(.2,.8,.2,1) both;text-align:left}
++.viz figcaption{margin-top:10px;font-size:11.5px;color:var(--text-muted);text-align:center}
++.viz-top{display:flex;align-items:center;gap:8px;font-size:13px;padding:2px 4px 12px}
++.viz-top b{font-weight:700}.viz-top .muted{color:var(--text-muted)}
++.viz-top .lat{margin-left:auto;font-size:12px;color:var(--site);background:rgba(var(--site-rgb),.1);border:1px solid rgba(var(--site-rgb),.3);padding:3px 9px;border-radius:999px}
++.viz-top .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;box-shadow:0 0 0 4px rgba(34,197,94,.18)}
++.viz-top .av{width:26px;height:26px;border-radius:8px;display:grid;place-items:center;background:rgba(var(--site-rgb),.15);color:var(--site);font-size:13px}
++.viz-video{position:relative;aspect-ratio:16/9;border-radius:14px;overflow:hidden;background:radial-gradient(120% 90% at 30% 20%,rgba(var(--site-rgb),.28),transparent 60%),linear-gradient(160deg,#1a2338,#0b1020 70%);border:1px solid var(--border)}
++.viz-video::after{content:'';position:absolute;left:10%;right:10%;bottom:0;height:42%;background:linear-gradient(transparent,rgba(0,0,0,.35)),repeating-linear-gradient(90deg,rgba(255,255,255,.05) 0 1px,transparent 1px 38px);transform:perspective(220px) rotateX(58deg);transform-origin:bottom}
++.viz-video .scan{position:absolute;inset:0;background:linear-gradient(transparent 0,rgba(255,255,255,.04) 50%,transparent 100%);background-size:100% 6px;opacity:.5}
++.viz-video .onair{position:absolute;top:10px;left:10px;font-size:11px;font-weight:700;padding:3px 8px;border-radius:6px;background:rgba(239,68,68,.9);color:#fff}.viz-video .onair i{font-size:7px;vertical-align:2px;margin-right:3px}
++.viz-video .cam{position:absolute;bottom:10px;right:10px;font-size:11px;color:#dbe4f5;background:rgba(0,0,0,.45);padding:3px 8px;border-radius:6px}
++.viz-ctrl{display:flex;gap:14px;align-items:center;margin-top:12px}
++.dpad{display:grid;grid-template-columns:repeat(3,34px);grid-template-rows:repeat(3,34px);gap:4px;flex:none}
++.dpad span{display:grid;place-items:center;border-radius:9px;background:var(--bg-hover);color:var(--text-primary);font-size:15px;border:1px solid var(--border)}
++.dpad .u{grid-area:1/2}.dpad .l{grid-area:2/1}.dpad .c{grid-area:2/2;background:rgba(var(--site-rgb),.15);border-color:rgba(var(--site-rgb),.35)}.dpad .r{grid-area:2/3}.dpad .d{grid-area:3/2}
++.dpad .u{background:rgba(var(--site-rgb),.22);border-color:rgba(var(--site-rgb),.5)}
++.viz-side{flex:1;min-width:0;display:grid;gap:8px}
++.viz-side .keys{display:flex;flex-wrap:wrap;gap:6px}
++.viz-side .keys span{font-size:11.5px;padding:5px 9px;border-radius:8px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
++.viz-side .meter{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--text-muted)}
++.viz-side .meter i{flex:1;height:6px;border-radius:9px;background:linear-gradient(90deg,#22c55e var(--v),var(--bg-hover) var(--v))}
++.viz-side .meter b{color:var(--text-secondary);font-weight:600}
++.viz-side .estop{justify-self:end;font-size:12px;font-weight:800;letter-spacing:.08em;color:#fff;background:#dc2626;border-radius:10px;padding:7px 16px;box-shadow:0 0 0 3px rgba(220,38,38,.25)}
++.feed{list-style:none;display:grid;gap:8px;padding:2px}
++.feed li{display:flex;align-items:flex-start;gap:10px;font-size:13.5px;line-height:1.45;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
++.feed li i{color:#22c55e;margin-top:3px}
++.feed li.ask{flex-wrap:wrap;border-color:rgba(var(--site-rgb),.45);background:rgba(var(--site-rgb),.08);color:var(--text-primary)}.feed li.ask i{color:var(--site)}.feed li.ask>span:first-of-type{flex:1;min-width:160px}
++.feed .pills{display:flex;gap:6px;margin-left:auto}.feed .pills span{font-size:12px;font-weight:600;padding:4px 11px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}.feed .pills .ok{background:var(--site);border-color:var(--site);color:#0b0d10}
++.steps{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;counter-reset:s}
++.steps li{position:relative;background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px 20px 20px;display:flex;flex-direction:column;gap:8px}
++.steps .n{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:14px;color:#0b0d10;background:var(--site)}
++.steps b{font-size:15px}.steps span:last-child{font-size:13px;color:var(--text-secondary);line-height:1.55}
++.grid.three{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
++.viz .task{font-size:14px;line-height:1.5;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary)}
++.viz .modes{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
++.viz .modes span{font-size:11.5px;font-weight:600;padding:4px 10px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}
++.viz .modes .on{background:var(--site);border-color:var(--site);color:#0b0d10}
++.cands{list-style:none;display:grid;gap:6px}
++.cands li{display:grid;grid-template-columns:18px minmax(0,1fr) 54px 50px 44px;align-items:center;gap:8px;font-size:12.5px;padding:8px 10px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary);position:relative}
++.cands li i{color:var(--text-muted)}.cands li b{font-weight:600;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
++.cands li span{text-align:right;font-variant-numeric:tabular-nums}
++.cands li.pick{border-color:rgba(var(--site-rgb),.55);background:rgba(var(--site-rgb),.09)}.cands li.pick i{color:var(--site)}
++.cands li em{position:absolute;top:-8px;right:10px;font-style:normal;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 7px;border-radius:999px;background:var(--site);color:#0b0d10}
++.viz .why{margin-top:10px;font-size:12.5px;line-height:1.5;color:var(--text-secondary)}.viz .why i{color:var(--site);margin-right:4px}
++.choice{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
++.card.big{padding:24px}.card.big b{font-size:17px}
++.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px}
++.chips span{font-size:12.5px;padding:6px 11px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-secondary)}
++.chips.big span{font-size:13.5px;padding:9px 14px;color:var(--text-primary)}.chips.big span i{color:var(--site);margin-right:8px}
++.build .card{border-style:dashed;background:rgba(0,0,0,.15)}.build .card b i{color:var(--site);margin-right:6px}.build .card span a{color:var(--text-secondary)}.build .card code{font-size:12px}
++@media (max-width:860px){.hero.split{grid-template-columns:1fr;text-align:center;gap:28px}.hero.split .ctas{justify-content:center}.hero.split p.lead{margin:0 auto}}
++@media (max-width:600px){.hero{padding-top:56px}.ctas .btn{width:100%;justify-content:center}.viz-ctrl{flex-direction:column;align-items:stretch}.dpad{align-self:center}.cands li{grid-template-columns:14px minmax(0,1fr) 30px 40px 30px;gap:6px;font-size:12px;padding:8px}}
++@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
++</style>
++</head>
++<body>
++<div class="orbs"><i></i><i></i><i></i></div>
++<div id="navbar-mount"></div>
++<main class="wrap">
++ <header class="hero">
++ <div class="mark"><i class="fa-solid fa-satellite-dish" aria-hidden="true"></i></div>
++ <div class="badge"><span class="ov-mark" data-size="16" data-variant="realtime"></span> Part of the OpenVibe network · <b>closed</b></div>
++ <h1><span class="d">realtime</span><span class="tld">.openvibe.network</span></h1>
++ <div class="tag">Closed: realtime delivery is part of OpenVibe.Events.</div>
++ <p class="lead">OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.</p>
++ <div class="ctas">
++ <a class="btn btn-primary" href="https://events.openvibe.network/"><i class="fa-solid fa-arrow-right"></i> OpenVibe.Events</a>
++ <a class="btn" href="https://openvibe.network/#network"><i class="fa-solid fa-circle-nodes"></i> The whole network</a>
++ <a class="btn" href="https://discord.gg/M6MuRUaeJj" rel="noopener"><i class="fa-brands fa-discord"></i> Follow the build</a>
++ </div>
++ <p class="status"><i class="fa-solid fa-clock" aria-hidden="true"></i> Status: <b>closed</b> · <strong>realtime.openvibe.network</strong> is closed and stays as a decision record; no product launches at this address. · as of 2026-09-29 · <a href="/status.json">status.json</a></p>
++ </header>
++
++
++ <section id="what" class="meanwhile">
++ <h2>Where to go</h2>
++ <p class="lead2">Realtime delivery for browsers is a part of OpenVibe.Events; the decision and its record are public.</p>
++ <div class="grid">
++ <a class="card" href="https://events.openvibe.network/"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>OpenVibe.Events</b><span>Durable events, subscriptions and signed delivery; browsers subscribe at /realtime/stream.</span><span class="go">events.openvibe.network →</span></a>
++ <a class="card" href="https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-005-realtime.md"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>ADR-005: realtime</b><span>The decision, and when a separate realtime service would be reconsidered.</span><span class="go">github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-005-realtime.md →</span></a>
++ </div>
++ </section>
++
++ <section class="net">
++ <h2>The OpenVibe network</h2>
++ <p class="lead2">One account for all of it. Sign in once and every site signs you in.</p>
++ <div class="grid">
++ <a class="card" href="https://openvibe.live"><div class="ic"><i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i></div><b>OpenVibe.Live</b><span>Go live, chat, clip, restream</span><span class="go">openvibe.live →</span></a>
++ <a class="card" href="https://openvibe.tools"><div class="ic"><i class="fa-solid fa-screwdriver-wrench" aria-hidden="true"></i></div><b>OpenVibe.Tools</b><span>Hundreds of online tools</span><span class="go">openvibe.tools →</span></a>
++ <a class="card" href="https://openvibe.community"><div class="ic"><i class="fa-solid fa-people-group" aria-hidden="true"></i></div><b>OpenVibe.Community</b><span>Pastes, posts, people</span><span class="go">openvibe.community →</span></a>
++ <a class="card" href="https://openvibe.games"><div class="ic"><i class="fa-solid fa-gamepad" aria-hidden="true"></i></div><b>OpenVibe.Games</b><span>Scraplandia and browser games</span><span class="go">openvibe.games →</span></a>
++ <a class="card" href="https://openvibe.media"><div class="ic"><i class="fa-solid fa-photo-film" aria-hidden="true"></i></div><b>OpenVibe.Media</b><span>VODs, clips and files</span><span class="go">openvibe.media →</span></a>
++ <a class="card" href="https://openvibe.network"><div class="ic"><i class="fa-solid fa-circle-nodes" aria-hidden="true"></i></div><b>OpenVibe.Network</b><span>One account, every site</span><span class="go">openvibe.network →</span></a>
++ </div>
++ <h2 style="margin-top:28px;font-size:16px">Other OpenVibe domains, not open yet</h2>
++ <div class="rooms" style="margin-top:8px">
++ <a href="https://openvibe.news/"><i class="fa-solid fa-newspaper" aria-hidden="true"></i>OpenVibe.News</a>
++ <a href="https://openvibe.reviews/"><i class="fa-solid fa-star-half-stroke" aria-hidden="true"></i>OpenVibe.Reviews</a>
++ <a href="https://openvibe.tips/"><i class="fa-solid fa-hand-holding-heart" aria-hidden="true"></i>OpenVibe.Tips</a>
++ <a href="https://openvibe.vip/"><i class="fa-solid fa-crown" aria-hidden="true"></i>OpenVibe.VIP</a>
++ <a href="https://openvibe.trade/"><i class="fa-solid fa-chart-line" aria-hidden="true"></i>OpenVibe.Trade</a>
++ <a href="https://openvibe.host/"><i class="fa-solid fa-server" aria-hidden="true"></i>OpenVibe.Host</a>
++ <a href="https://openvibe.deals/"><i class="fa-solid fa-tags" aria-hidden="true"></i>OpenVibe.Deals</a>
++ <a href="https://openvibe.coupons/"><i class="fa-solid fa-ticket" aria-hidden="true"></i>OpenVibe.Coupons</a>
++ <a href="https://openre.stream/"><i class="fa-solid fa-satellite-dish" aria-hidden="true"></i>OpenRe.Stream</a>
++ <a href="https://auth.openvibe.network/"><i class="fa-solid fa-key" aria-hidden="true"></i>auth.openvibe.network</a>
++ <a href="https://api.openvibe.network/"><i class="fa-solid fa-plug" aria-hidden="true"></i>api.openvibe.network</a>
++ <a href="https://admin.openvibe.network/"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i>admin.openvibe.network</a>
++ <a href="https://themes.openvibe.network/"><i class="fa-solid fa-palette" aria-hidden="true"></i>themes.openvibe.network</a>
++ <a href="https://ai.openvibe.services/"><i class="fa-solid fa-robot" aria-hidden="true"></i>ai.openvibe.services</a>
++ <a href="https://openvibe.pics/"><i class="fa-solid fa-images" aria-hidden="true"></i>OpenVibe.Pics</a>
++ <a href="https://openvibe.download/"><i class="fa-solid fa-cloud-arrow-down" aria-hidden="true"></i>OpenVibe.Download</a>
++ <a href="https://openvibe.space/"><i class="fa-solid fa-layer-group" aria-hidden="true"></i>OpenVibe.Space</a>
++ <a href="https://openvibe.food/"><i class="fa-solid fa-utensils" aria-hidden="true"></i>OpenVibe.Food</a>
++ <a href="https://openvibe.quest/"><i class="fa-solid fa-map" aria-hidden="true"></i>OpenVibe.Quest</a>
++ <a href="https://openvibe.rent/"><i class="fa-solid fa-key" aria-hidden="true"></i>OpenVibe.Rent</a>
++ <a href="https://openvibe.homes/"><i class="fa-solid fa-house" aria-hidden="true"></i>OpenVibe.Homes</a>
++ <a href="https://openvibe.services/"><i class="fa-solid fa-server" aria-hidden="true"></i>OpenVibe.Services</a>
++ <a href="https://openvibe.run/"><i class="fa-solid fa-terminal" aria-hidden="true"></i>OpenVibe.Run</a>
++ <a href="https://openvibe.work/"><i class="fa-solid fa-briefcase" aria-hidden="true"></i>OpenVibe.Work</a>
++ <a href="https://openvibe.zone/"><i class="fa-solid fa-box-archive" aria-hidden="true"></i>OpenVibe.Zone</a>
++ <a href="https://openvibe.video/"><i class="fa-solid fa-film" aria-hidden="true"></i>OpenVibe.Video</a>
++ <a href="https://openvibe.website/"><i class="fa-solid fa-globe" aria-hidden="true"></i>OpenVibe.Website</a>
++ <a href="https://openvibe.help/"><i class="fa-solid fa-life-ring" aria-hidden="true"></i>OpenVibe.Help</a>
++ <a href="https://openvibe.bot/"><i class="fa-solid fa-robot" aria-hidden="true"></i>OpenVibe.Bot</a>
++ <a href="https://openvibe.watch/"><i class="fa-solid fa-eye" aria-hidden="true"></i>OpenVibe.Watch</a>
++ <a href="https://openvibe.actor/"><i class="fa-solid fa-user-astronaut" aria-hidden="true"></i>OpenVibe.Actor</a>
++ <a href="https://openvibe.events/"><i class="fa-solid fa-bolt" aria-hidden="true"></i>OpenVibe.Events</a>
++ </div>
++ </section>
++
++ <div class="strip">
++ <span class="ov-mark" data-size="44" data-variant="realtime"></span>
++ <h3>Open source. Community run.</h3>
++ <p>Every OpenVibe site is built in the open by the people using it — free speech within the rules, no gatekeeping, one identity that follows you everywhere.</p>
++ <a class="btn" href="https://github.com/OpenVibers" rel="noopener"><i class="fa-brands fa-github"></i> OpenVibers on GitHub</a>
++ </div>
++</main>
++<style id="ovf-css">
++.ovf{background:var(--bg-secondary,#12131c);border-top:1px solid var(--border,rgba(255,255,255,.08));margin-top:40px;
++ padding:clamp(24px,4vw,40px) clamp(14px,4vw,32px) calc(clamp(20px,3vw,28px) + env(safe-area-inset-bottom,0px));}
++.ovf[data-variant="compact"]{margin-top:24px;padding:14px clamp(12px,3vw,24px) calc(14px + env(safe-area-inset-bottom,0px));}
++.ovf-inner{max-width:1240px;margin:0 auto}
++.ovf-inner--compact{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px 18px;text-align:center}
++.ovf-cols{display:grid;gap:clamp(20px,3vw,36px);grid-template-columns:minmax(230px,1.6fr) repeat(auto-fit,minmax(120px,1fr));align-items:start}
++.ovf-col{display:flex;flex-direction:column;gap:8px;min-width:0}
++.ovf-col h3{margin:0 0 2px;font-size:.72rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted,#8b93ad)}
++.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}
++.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
++.ovf-more{font-weight:600;opacity:.85}.ovf-ic{flex:none;vertical-align:-.14em}.ovf-more .ovf-ic{font-size:.85em;margin-left:3px;transition:transform .15s}.ovf-more:hover .ovf-ic{transform:translateX(3px)}.ovf-social .ovf-ic{font-size:17px}.ovf-account-links .ovf-ic{font-size:1.05em;opacity:.85}
++.ovf-brand{display:inline-flex;align-items:center;gap:9px;color:var(--text-primary,#f1f4fb);font-size:1.05rem;font-weight:700;text-decoration:none}
++.ovf-brand--sm{font-size:.9rem}
++.ovf-tagline{margin:0;font-size:.82rem;line-height:1.55;color:var(--text-muted,#8b93ad);max-width:46ch}
++.ovf-social{display:flex;gap:8px;margin-top:4px}
++.ovf-social a{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--text-secondary,#b6bdd2);
++ background:var(--bg-tertiary,#1a1b28);border:1px solid var(--border,rgba(255,255,255,.08));font-size:.95rem;transition:color .15s,border-color .15s,transform .15s}
++.ovf-social a:hover{color:var(--accent,#60a5fa);border-color:var(--accent,#60a5fa);transform:translateY(-2px)}
++.ovf-account{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 18px;
++ margin-top:clamp(18px,2.5vw,26px);padding:10px 14px;border-radius:14px;
++ background:color-mix(in srgb, var(--accent,#60a5fa) 8%, var(--bg-tertiary,#1a1b28));
++ border:1px solid color-mix(in srgb, var(--accent,#60a5fa) 26%, var(--border,rgba(255,255,255,.08)));
++ animation:ovfIn .4s ease-out both}
++@keyframes ovfIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
++.ovf-account-who{display:inline-flex;align-items:center;gap:8px;font-size:.84rem;color:var(--text-secondary,#b6bdd2)}
++.ovf-account-who strong{color:var(--text-primary,#f1f4fb)}
++.ovf-avatar{width:24px;height:24px;border-radius:50%;overflow:hidden;display:grid;place-items:center;
++ background:var(--accent,#60a5fa);color:#fff;font-size:.7rem;font-weight:800;flex:none}
++.ovf-avatar img{width:100%;height:100%;object-fit:cover}
++.ovf-account-links{display:flex;flex-wrap:wrap;gap:6px}
++.ovf-account-links a{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;font-size:.78rem;font-weight:600;
++ color:var(--text-secondary,#b6bdd2);background:var(--bg-secondary,#12131c);border:1px solid var(--border,rgba(255,255,255,.08));
++ text-decoration:none;transition:color .15s,border-color .15s,transform .15s}
++.ovf-account-links a:hover{color:var(--accent,#60a5fa);border-color:var(--accent,#60a5fa);transform:translateY(-1px)}
++.ovf-account-links i{font-size:.72rem;opacity:.8}
++.ovf-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;
++ margin-top:clamp(20px,3vw,30px);padding-top:16px;border-top:1px solid var(--border,rgba(255,255,255,.08));
++ padding-inline:clamp(0px, calc(84px - (100vw - 1240px) / 2), 84px)}
++.ovf-copy{color:var(--text-muted,#8b93ad);font-size:.78rem}
++.ovf-legal-inline,.ovf-compact-links{display:flex;flex-wrap:wrap;gap:14px}
++.ovf [hidden],.ovf-account[hidden]{display:none!important}
++.ovf .ovf-shipped.ov-shipped-latest{font-size:.76rem;padding:4px 12px;max-width:min(520px,100%);background:transparent}
++.ovf-legal-inline a,.ovf-compact-links a{color:var(--text-muted,#8b93ad);font-size:.78rem;text-decoration:none;transition:color .15s}
++.ovf-legal-inline a:hover,.ovf-compact-links a:hover{color:var(--accent,#60a5fa)}
++.ovf-compact-meta{color:var(--text-muted,#8b93ad);font-size:.74rem}
++@media (max-width:900px){.ovf-cols{grid-template-columns:repeat(2,minmax(0,1fr))}.ovf-col--brand{grid-column:1/-1}}
++@media (max-width:560px){
++ .ovf{padding-bottom:calc(20px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
++ .ovf[data-variant="compact"]{padding-bottom:calc(14px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
++ .ovf-cols{gap:22px 16px}
++ .ovf-bar{justify-content:center;text-align:center;padding-inline:0}
++ .ovf-legal-inline{justify-content:center}
++ .ovf-account{flex-direction:column;align-items:flex-start}
++}
++@media (prefers-reduced-motion:reduce){.ovf-account{animation:none}.ovf-col a:hover,.ovf-social a:hover,.ovf-account-links a:hover{transform:none}}
++.ovf{-webkit-tap-highlight-color:transparent}
++:where(.ovf) :where(a,button):active{scale:.97}
++:where(.ovf) :where(a,button):focus-visible{outline:2px solid var(--accent-light,var(--accent,#3b82f6));outline-offset:2px;border-radius:4px}
++@media (prefers-reduced-motion:reduce){:where(.ovf) :where(a,button):active{scale:none;opacity:.82}}</style><footer id="ov-footer" class="ovf" data-variant="full"><div class="ovf-inner">
++ <div class="ovf-cols">
++ <div class="ovf-col ovf-col--brand">
++ <a class="ovf-brand" href="/"><span class="ov-mark" data-size="26"></span><span>OpenVibe</span></a>
++ <p class="ovf-tagline">Open source and community run. Built in the open by the people using it.</p>
++ <div class="ovf-social">
++ <a href="https://github.com/OpenVibers" target="_blank" rel="noopener" aria-label="GitHub" title="GitHub"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path fill="currentColor" stroke="none" d="M12 2.2a9.8 9.8 0 0 0-3.1 19.1c.5.1.7-.2.7-.5v-1.7c-2.7.6-3.3-1.2-3.3-1.2-.4-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.2-.2-4.5-1.1-4.5-4.8 0-1.1.4-1.9 1-2.6-.1-.2-.4-1.2.1-2.6 0 0 .8-.3 2.7 1a9.3 9.3 0 0 1 4.9 0c1.9-1.3 2.7-1 2.7-1 .5 1.4.2 2.4.1 2.6.6.7 1 1.5 1 2.6 0 3.7-2.3 4.6-4.5 4.8.4.3.7.9.7 1.800v2.700c0 .3.2.6.7.5A9.8 9.8 0 0 0 12 2.200z"/></svg></a><a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener" aria-label="Discord" title="Discord"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path fill="currentColor" stroke="none" d="M19.3 5.300a16.5 16.5 0 0 0-4.1-1.300l-.5 1a15.3 15.3 0 0 0-5.4 0l-.5-1a16.5 16.5 0 0 0-4.1 1.300C2.1 9.2 1.4 13 1.7 16.700a16.6 16.6 0 0 0 5.1 2.600l1.1-1.800c-.6-.2-1.2-.5-1.7-.8l.4-.3a11.8 11.8 0 0 0 10.8 0l.4.3c-.5.3-1.1.6-1.7.8l1.1 1.800a16.6 16.6 0 0 0 5.1-2.600c.4-4.3-.7-8.1-3-11.400zM8.7 14.400c-1 0-1.8-.9-1.8-2.100s.8-2.1 1.8-2.1 1.9 1 1.8 2.100c0 1.2-.8 2.1-1.8 2.100zm6.6 0c-1 0-1.8-.9-1.8-2.100s.8-2.1 1.8-2.1 1.9 1 1.8 2.100c0 1.2-.8 2.1-1.8 2.100z"/></svg></a>
++ <a href="/sitemap.xml" aria-label="Sitemap" title="Sitemap"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="3.5" width="6" height="4.5" rx="1"/><rect x="3" y="16" width="6" height="4.5" rx="1"/><rect x="15" y="16" width="6" height="4.5" rx="1"/><path d="M12 8v4M6 16v-2.500h12V16"/></svg></a>
++ </div>
++ </div>
++
++ <nav class="ovf-col" aria-label="The OpenVibe network">
++ <h3>Network</h3>
++ <a href="https://openvibe.live" title="Go live, restream and clip">Live streaming</a><a href="https://openvibe.tools" title="The whole toolbox">Online tools</a><a href="https://dev.openvibe.tools" title="Formatters, encoders, validators">Developer tools</a><a href="https://net.openvibe.tools" title="Ping, DNS, Whois, SSL">Network diagnostics</a><a href="https://img.openvibe.tools" title="Convert, resize, compress">Image converter</a><a href="https://audio.openvibe.tools" title="Convert, trim, pitch-shift">Audio converter</a>
++ <a class="ovf-more" href="https://openvibe.network" target="_blank" rel="noopener">Everything else <svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
++ </nav>
++
++ <nav class="ovf-col" aria-label="Legal">
++ <h3>Legal</h3>
++ <a href="https://openvibe.live/tos">Terms of Service</a><a href="https://openvibe.live/privacy">Privacy Policy</a><a href="https://openvibe.live/dmca">DMCA</a>
++ </nav>
++ </div>
++ <div class="ovf-account" id="ovf-account" hidden></div>
++ <div class="ovf-bar">
++ <span class="ovf-copy">Open source &amp; community driven · built in the open</span>
++ <a class="ovf-shipped" data-ov-shipped="latest" data-service="realtime" href="https://openvibe.network/updates?site=realtime" hidden></a>
++ <span class="ovf-legal-inline">
++ <a href="https://openvibe.network/updates?site=realtime">Updates</a>
++ <a href="https://openvibe.live/tos">Terms</a><a href="https://openvibe.live/privacy">Privacy</a><a href="https://openvibe.live/dmca">DMCA</a>
++ </span>
++ </div>
++ </div></footer>
++<script src="/shared/ov-mark.js?v=03f3596adcab" async></script>
++<script src="/shared/navbar.js?v=017a740747b4"></script>
++<script src="/shared/footer.js?v=a85ab893be88"></script>
++<script>
++(function () {
++ if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'realtime', apiBase: 'https://openvibe.network', links: [{ label: 'Where to go', href: '#what' }, { label: 'The network', href: 'https://openvibe.network/#network', external: false }], history: { type: 'page', title: 'realtime.openvibe.network' } }); } catch (e) {} }
++ if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: 'realtime', variant: 'full', links: [{ heading: 'realtime.openvibe.network', items: [{ name: 'Sign in', url: 'https://openvibe.network/login?return=https%3A%2F%2Frealtime.openvibe.network%2F' }, { name: 'The network', url: 'https://openvibe.network/#network' }, { name: 'Discord', url: 'https://discord.gg/M6MuRUaeJj' }] }] }); } catch (e) {} }
++})();
++</script>
++</body>
++</html>
ddiff --git a/notice-pages/status.openvibe.network.html b/notice-pages/status.openvibe.network.html
nnew file mode 100644
iindex 0000000..713072e
---- /dev/null
++++ b/notice-pages/status.openvibe.network.html
@@@ -0,0 +1,373 @@
++<!DOCTYPE html>
++<html lang="en">
++<head>
++<meta charset="utf-8">
++<meta name="viewport" content="width=device-width, initial-scale=1">
++<title>status.openvibe.network — Is OpenVibe up?</title>
++<meta name="description" content="OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.">
++<meta name="keywords" content="openvibe status, openvibe uptime, openvibe network status">
++<meta name="robots" content="noindex, follow">
++<meta name="theme-color" content="#22c55e">
++<link rel="canonical" href="https://status.openvibe.network/">
++<link rel="icon" type="image/svg+xml" data-ov-icon="network" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-32%20-32%2064%2064%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22ring%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2360a5fa%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%232563eb%22%2F%3E%3C%2FlinearGradient%3E%0A%3CradialGradient%20id%3D%22glow%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2360a5fa%22%20stop-opacity%3D%22.34%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2360a5fa%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%0A%3CradialGradient%20id%3D%22bg%22%20cx%3D%22.5%22%20cy%3D%22.38%22%20r%3D%22.8%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%2316223a%22%2F%3E%3Cstop%20offset%3D%22.6%22%20stop-color%3D%22%230a0f1c%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23060912%22%2F%3E%3C%2FradialGradient%3E%3C%2Fdefs%3E%3Crect%20x%3D%22-32%22%20y%3D%22-32%22%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2215%22%20fill%3D%22url(%23bg)%22%2F%3E%3Cg%20transform%3D%22scale(1.22)%22%3E%0A%3Ccircle%20r%3D%2221%22%20fill%3D%22url(%23glow)%22%2F%3E%0A%3Ccircle%20r%3D%2218%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224%22%20opacity%3D%22.38%22%2F%3E%0A%3Cpath%20d%3D%22M0%2C-18%20A18%2C18%200%200%2C1%2017.4%2C4.6%22%20fill%3D%22none%22%20stroke%3D%22url(%23ring)%22%20stroke-width%3D%224.4%22%20stroke-linecap%3D%22round%22%2F%3E%0A%3Cpath%20d%3D%22M-9.5%2C-7%20L0%2C10%20L9.5%2C-7%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%224.8%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3Ccircle%20cx%3D%2217.4%22%20cy%3D%224.6%22%20r%3D%223.2%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E">
++<style id="ov-critical">html{background:#0a0f1c;color:#e6edf7;color-scheme:dark}</style><script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>
++<link rel="manifest" href="/manifest.webmanifest">
++<link rel="alternate" type="text/html" hreflang="en" href="https://status.openvibe.network/">
++<meta property="og:type" content="website">
++<meta property="og:site_name" content="status.openvibe.network">
++<meta property="og:url" content="https://status.openvibe.network/">
++<meta property="og:title" content="status.openvibe.network — Is OpenVibe up?">
++<meta property="og:description" content="OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.">
++<meta property="og:image" content="https://openvibe.network/assets/logo-512.png">
++<meta property="og:image:alt" content="OpenVibe">
++<meta name="twitter:card" content="summary">
++<meta name="twitter:title" content="status.openvibe.network — Is OpenVibe up?">
++<meta name="twitter:description" content="OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.">
++<meta name="twitter:image" content="https://openvibe.network/assets/logo-512.png">
++<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://status.openvibe.network/#site","url":"https://status.openvibe.network/","name":"status.openvibe.network","description":"OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.","inLanguage":"en","publisher":{"@type":"Organization","@id":"https://openvibe.network/#org","name":"OpenVibe","url":"https://openvibe.network/","logo":"https://openvibe.network/assets/logo-512.png","sameAs":["https://github.com/OpenVibers","https://discord.gg/M6MuRUaeJj"]},"isPartOf":{"@type":"WebSite","@id":"https://openvibe.network/#site","name":"OpenVibe.Network","url":"https://openvibe.network/"}},{"@type":"WebPage","@id":"https://status.openvibe.network/#page","url":"https://status.openvibe.network/","name":"status.openvibe.network — Is OpenVibe up?","description":"OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.","isPartOf":{"@id":"https://status.openvibe.network/#site"},"datePublished":"2026-09-17","dateModified":"2026-10-03","breadcrumb":{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"OpenVibe","item":"https://openvibe.network/"},{"@type":"ListItem","position":2,"name":"status.openvibe.network","item":"https://status.openvibe.network/"}]}}]}</script>
++<script>(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script>
++<script src="/shared/theme-loader.js?v=ee4a6754d0f7" defer></script>
++<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
++<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" media="print" onload="this.media='all'">
++<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"></noscript>
++<style>
++*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
++:root{--bg-primary:#0a0f1c;--bg-secondary:#101828;--bg-card:#131c2e;--bg-hover:#1c2a44;--border:#1f2d47;--text-primary:#e6edf7;--text-secondary:#96a7c2;--text-muted:#7386a3;--accent:#3b82f6;--accent-light:#60a5fa;--accent-rgb:59,130,246;--on-accent:#fff;--site:#22c55e;--site-rgb:34,197,94}
++html{scroll-behavior:smooth}
++body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden}
++main{flex:1;position:relative}
++.orbs{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
++.orbs i{position:absolute;border-radius:50%;filter:blur(90px);opacity:.16;animation:drift 26s ease-in-out infinite alternate}
++.orbs i:nth-child(1){width:420px;height:420px;background:var(--site);top:-140px;left:-120px}
++.orbs i:nth-child(2){width:360px;height:360px;background:var(--accent);top:40%;right:-140px;animation-delay:-9s}
++.orbs i:nth-child(3){width:300px;height:300px;background:var(--site);bottom:-120px;left:38%;animation-delay:-17s}
++@keyframes drift{0%{transform:translate(0,0) scale(1)}50%{transform:translate(-26px,36px) scale(1.07)}100%{transform:translate(34px,-28px) scale(.95)}}
++.wrap{position:relative;z-index:1;max-width:1040px;margin:0 auto;padding:0 20px}
++.hero{padding:84px 0 44px;text-align:center}
++.hero .mark{display:inline-grid;place-items:center;width:84px;height:84px;border-radius:24px;background:rgba(var(--site-rgb),.12);border:1px solid rgba(var(--site-rgb),.35);color:var(--site);font-size:34px;margin-bottom:22px;box-shadow:0 20px 60px rgba(var(--site-rgb),.18);animation:rise .6s cubic-bezier(.2,.8,.2,1) both}
++.hero .badge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-size:12px;font-weight:600;color:var(--text-secondary);letter-spacing:.3px;margin-bottom:22px;animation:rise .6s .05s cubic-bezier(.2,.8,.2,1) both}
++.hero .badge b{color:var(--site)}
++.hero h1{font-size:clamp(34px,6vw,58px);font-weight:800;letter-spacing:-1.4px;line-height:1.08;animation:rise .6s .1s cubic-bezier(.2,.8,.2,1) both}
++.hero h1 .d{color:var(--site)}
++.hero h1 .tld{color:var(--text-secondary)}
++.hero .tag{font-size:clamp(18px,2.6vw,24px);font-weight:600;color:var(--text-primary);margin:18px 0 10px;animation:rise .6s .16s cubic-bezier(.2,.8,.2,1) both}
++.hero p.lead{color:var(--text-secondary);font-size:16px;line-height:1.65;max-width:680px;margin:0 auto;animation:rise .6s .22s cubic-bezier(.2,.8,.2,1) both}
++.ctas{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:30px;animation:rise .6s .3s cubic-bezier(.2,.8,.2,1) both}
++.hero .status{margin:22px auto 0;max-width:720px;font-size:12.5px;line-height:1.7;color:var(--text-muted);border:1px dashed var(--border);border-radius:12px;padding:10px 14px;background:rgba(0,0,0,.15)}
++.hero .status b{color:var(--site)}
++.hero .status code{font-size:12px;color:var(--text-secondary);background:var(--bg-secondary);padding:1px 6px;border-radius:6px}
++.hero .status a{color:var(--text-secondary)}
++.btn{display:inline-flex;align-items:center;gap:9px;padding:12px 20px;border-radius:12px;font-size:14px;font-weight:600;text-decoration:none;border:1px solid var(--border);color:var(--text-primary);background:var(--bg-secondary);transition:transform .15s,border-color .15s,box-shadow .15s}
++.btn:hover{transform:translateY(-1px);border-color:var(--site);box-shadow:0 10px 30px rgba(var(--site-rgb),.15)}
++.btn-primary{background:var(--site);border-color:var(--site);color:#0b0d10}
++.btn-primary:hover{filter:brightness(1.08)}
++@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
++section{padding:36px 0}
++h2{font-size:22px;font-weight:700;letter-spacing:-.3px;margin-bottom:6px}
++.lead2{color:var(--text-secondary);font-size:14px;margin-bottom:20px}
++.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
++.card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px;transition:transform .2s,border-color .2s,box-shadow .2s;text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:8px}
++a.card:hover{transform:translateY(-3px);border-color:rgba(var(--site-rgb),.6);box-shadow:0 16px 40px rgba(0,0,0,.35)}
++.card .ic{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;background:rgba(var(--site-rgb),.12);color:var(--site);font-size:17px}
++.card b{font-size:15px}
++.card span{font-size:13px;color:var(--text-secondary);line-height:1.55}
++.card .go{margin-top:auto;font-size:12.5px;font-weight:600;color:var(--site)}
++.highlight .hl-card{border:1px solid rgba(var(--accent-rgb),.45);background:rgba(var(--accent-rgb),.10);border-radius:18px;padding:22px 22px 18px}
++.highlight h2{margin:0 0 8px}.highlight h2 i{color:var(--accent-light)}.highlight p{margin:0 0 10px}
++.highlight ul{margin:0 0 14px;padding-left:20px}.highlight li{margin:4px 0}
++.faq details{border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px 14px;margin:8px 0}
++.faq summary{cursor:pointer;font-weight:600}.faq details p{margin:8px 0 0;opacity:.85}
++.meanwhile .card .ic{background:rgba(var(--accent-rgb),.12);color:var(--accent-light)}
++.net .card .ic{background:var(--bg-hover);color:var(--text-primary)}
++.rooms{display:flex;flex-wrap:wrap;gap:8px}
++.rooms a{font-size:12.5px;padding:7px 12px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary);text-decoration:none;transition:all .15s;background:var(--bg-secondary)}
++.rooms a:hover{color:var(--text-primary);border-color:var(--site)}
++.rooms a i{margin-right:6px;opacity:.8}
++.strip{border:1px solid var(--border);border-radius:18px;background:var(--bg-secondary);padding:30px 26px;text-align:center;margin:10px 0 50px}
++.strip h3{font-size:19px;font-weight:700;margin:12px 0 8px}
++.strip p{font-size:14px;color:var(--text-secondary);max-width:600px;margin:0 auto 18px;line-height:1.6}
++.strip .ov-mark{width:44px;height:44px}
++.hero.split{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:40px;align-items:center;text-align:left;padding-top:64px}
++.hero.split .ctas{justify-content:flex-start}.hero.split p.lead{margin:0}
++.viz{background:var(--bg-card);border:1px solid var(--border);border-radius:20px;padding:14px;box-shadow:0 30px 80px rgba(0,0,0,.45),0 0 0 1px rgba(var(--site-rgb),.08);animation:rise .7s .25s cubic-bezier(.2,.8,.2,1) both;text-align:left}
++.viz figcaption{margin-top:10px;font-size:11.5px;color:var(--text-muted);text-align:center}
++.viz-top{display:flex;align-items:center;gap:8px;font-size:13px;padding:2px 4px 12px}
++.viz-top b{font-weight:700}.viz-top .muted{color:var(--text-muted)}
++.viz-top .lat{margin-left:auto;font-size:12px;color:var(--site);background:rgba(var(--site-rgb),.1);border:1px solid rgba(var(--site-rgb),.3);padding:3px 9px;border-radius:999px}
++.viz-top .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;box-shadow:0 0 0 4px rgba(34,197,94,.18)}
++.viz-top .av{width:26px;height:26px;border-radius:8px;display:grid;place-items:center;background:rgba(var(--site-rgb),.15);color:var(--site);font-size:13px}
++.viz-video{position:relative;aspect-ratio:16/9;border-radius:14px;overflow:hidden;background:radial-gradient(120% 90% at 30% 20%,rgba(var(--site-rgb),.28),transparent 60%),linear-gradient(160deg,#1a2338,#0b1020 70%);border:1px solid var(--border)}
++.viz-video::after{content:'';position:absolute;left:10%;right:10%;bottom:0;height:42%;background:linear-gradient(transparent,rgba(0,0,0,.35)),repeating-linear-gradient(90deg,rgba(255,255,255,.05) 0 1px,transparent 1px 38px);transform:perspective(220px) rotateX(58deg);transform-origin:bottom}
++.viz-video .scan{position:absolute;inset:0;background:linear-gradient(transparent 0,rgba(255,255,255,.04) 50%,transparent 100%);background-size:100% 6px;opacity:.5}
++.viz-video .onair{position:absolute;top:10px;left:10px;font-size:11px;font-weight:700;padding:3px 8px;border-radius:6px;background:rgba(239,68,68,.9);color:#fff}.viz-video .onair i{font-size:7px;vertical-align:2px;margin-right:3px}
++.viz-video .cam{position:absolute;bottom:10px;right:10px;font-size:11px;color:#dbe4f5;background:rgba(0,0,0,.45);padding:3px 8px;border-radius:6px}
++.viz-ctrl{display:flex;gap:14px;align-items:center;margin-top:12px}
++.dpad{display:grid;grid-template-columns:repeat(3,34px);grid-template-rows:repeat(3,34px);gap:4px;flex:none}
++.dpad span{display:grid;place-items:center;border-radius:9px;background:var(--bg-hover);color:var(--text-primary);font-size:15px;border:1px solid var(--border)}
++.dpad .u{grid-area:1/2}.dpad .l{grid-area:2/1}.dpad .c{grid-area:2/2;background:rgba(var(--site-rgb),.15);border-color:rgba(var(--site-rgb),.35)}.dpad .r{grid-area:2/3}.dpad .d{grid-area:3/2}
++.dpad .u{background:rgba(var(--site-rgb),.22);border-color:rgba(var(--site-rgb),.5)}
++.viz-side{flex:1;min-width:0;display:grid;gap:8px}
++.viz-side .keys{display:flex;flex-wrap:wrap;gap:6px}
++.viz-side .keys span{font-size:11.5px;padding:5px 9px;border-radius:8px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
++.viz-side .meter{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--text-muted)}
++.viz-side .meter i{flex:1;height:6px;border-radius:9px;background:linear-gradient(90deg,#22c55e var(--v),var(--bg-hover) var(--v))}
++.viz-side .meter b{color:var(--text-secondary);font-weight:600}
++.viz-side .estop{justify-self:end;font-size:12px;font-weight:800;letter-spacing:.08em;color:#fff;background:#dc2626;border-radius:10px;padding:7px 16px;box-shadow:0 0 0 3px rgba(220,38,38,.25)}
++.feed{list-style:none;display:grid;gap:8px;padding:2px}
++.feed li{display:flex;align-items:flex-start;gap:10px;font-size:13.5px;line-height:1.45;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary)}
++.feed li i{color:#22c55e;margin-top:3px}
++.feed li.ask{flex-wrap:wrap;border-color:rgba(var(--site-rgb),.45);background:rgba(var(--site-rgb),.08);color:var(--text-primary)}.feed li.ask i{color:var(--site)}.feed li.ask>span:first-of-type{flex:1;min-width:160px}
++.feed .pills{display:flex;gap:6px;margin-left:auto}.feed .pills span{font-size:12px;font-weight:600;padding:4px 11px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}.feed .pills .ok{background:var(--site);border-color:var(--site);color:#0b0d10}
++.steps{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;counter-reset:s}
++.steps li{position:relative;background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px 20px 20px;display:flex;flex-direction:column;gap:8px}
++.steps .n{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:14px;color:#0b0d10;background:var(--site)}
++.steps b{font-size:15px}.steps span:last-child{font-size:13px;color:var(--text-secondary);line-height:1.55}
++.grid.three{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
++.viz .task{font-size:14px;line-height:1.5;padding:10px 12px;border-radius:12px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary)}
++.viz .modes{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
++.viz .modes span{font-size:11.5px;font-weight:600;padding:4px 10px;border-radius:999px;border:1px solid var(--border);color:var(--text-secondary)}
++.viz .modes .on{background:var(--site);border-color:var(--site);color:#0b0d10}
++.cands{list-style:none;display:grid;gap:6px}
++.cands li{display:grid;grid-template-columns:18px minmax(0,1fr) 54px 50px 44px;align-items:center;gap:8px;font-size:12.5px;padding:8px 10px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary);position:relative}
++.cands li i{color:var(--text-muted)}.cands li b{font-weight:600;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
++.cands li span{text-align:right;font-variant-numeric:tabular-nums}
++.cands li.pick{border-color:rgba(var(--site-rgb),.55);background:rgba(var(--site-rgb),.09)}.cands li.pick i{color:var(--site)}
++.cands li em{position:absolute;top:-8px;right:10px;font-style:normal;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 7px;border-radius:999px;background:var(--site);color:#0b0d10}
++.viz .why{margin-top:10px;font-size:12.5px;line-height:1.5;color:var(--text-secondary)}.viz .why i{color:var(--site);margin-right:4px}
++.choice{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
++.card.big{padding:24px}.card.big b{font-size:17px}
++.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px}
++.chips span{font-size:12.5px;padding:6px 11px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-secondary)}
++.chips.big span{font-size:13.5px;padding:9px 14px;color:var(--text-primary)}.chips.big span i{color:var(--site);margin-right:8px}
++.build .card{border-style:dashed;background:rgba(0,0,0,.15)}.build .card b i{color:var(--site);margin-right:6px}.build .card span a{color:var(--text-secondary)}.build .card code{font-size:12px}
++@media (max-width:860px){.hero.split{grid-template-columns:1fr;text-align:center;gap:28px}.hero.split .ctas{justify-content:center}.hero.split p.lead{margin:0 auto}}
++@media (max-width:600px){.hero{padding-top:56px}.ctas .btn{width:100%;justify-content:center}.viz-ctrl{flex-direction:column;align-items:stretch}.dpad{align-self:center}.cands li{grid-template-columns:14px minmax(0,1fr) 30px 40px 30px;gap:6px;font-size:12px;padding:8px}}
++@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
++.live .sum{font-size:14px;color:var(--text-secondary);margin-bottom:14px;line-height:1.6}
++.live .sum b{color:var(--text-primary)}
++.live .tbl{overflow-x:auto;border:1px solid var(--border);border-radius:14px;background:var(--bg-card)}
++.live table{width:100%;border-collapse:collapse;font-size:13px}
++.live th,.live td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
++.live th{color:var(--text-muted);font-weight:600;font-size:12px}
++.live tr:last-child td{border-bottom:0}
++.live td.st-up{color:#4ade80}.live td.st-degraded{color:#facc15}.live td.st-down{color:#f87171}.live td.st-not-running,.live td.st-unknown{color:var(--text-muted)}
++</style>
++</head>
++<body>
++<div class="orbs"><i></i><i></i><i></i></div>
++<div id="navbar-mount"></div>
++<main class="wrap">
++ <header class="hero">
++ <div class="mark"><i class="fa-solid fa-heart-pulse" aria-hidden="true"></i></div>
++ <div class="badge"><span class="ov-mark" data-size="16" data-variant="status"></span> Part of the OpenVibe network · <b>status</b></div>
++ <h1><span class="d">status</span><span class="tld">.openvibe.network</span></h1>
++ <div class="tag">Is OpenVibe up?</div>
++ <p class="lead">OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.</p>
++ <div class="ctas">
++ <a class="btn btn-primary" href="https://openvibe.network/status"><i class="fa-solid fa-arrow-right"></i> Status page</a>
++ <a class="btn" href="https://openvibe.network/#network"><i class="fa-solid fa-circle-nodes"></i> The whole network</a>
++ <a class="btn" href="https://discord.gg/M6MuRUaeJj" rel="noopener"><i class="fa-brands fa-discord"></i> Follow the build</a>
++ </div>
++ <p class="status"><i class="fa-solid fa-clock" aria-hidden="true"></i> Status: <b>status</b> · <a href="https://github.com/OpenVibers/OpenVibe.Network" rel="noopener"><code>OpenVibers/OpenVibe.Network</code></a> publishes the status of every OpenVibe service at <a href="https://openvibe.network/status">openvibe.network/status</a>; this address points there. · as of 2026-09-29 · <a href="/status.json">status.json</a></p>
++ </header>
++
++
++ <section id="what" class="meanwhile">
++ <h2>Where to go</h2>
++ <p class="lead2">Network&#39;s own observations, stated as seen: a service that has not been checked recently shows as unknown, never as up.</p>
++ <div class="grid">
++ <a class="card" href="https://openvibe.network/status"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>Status page</b><span>Every service: up, degraded, down, not running or unknown, where it runs and when it was last checked.</span><span class="go">openvibe.network/status →</span></a>
++ <a class="card" href="https://openvibe.network/api/v1/status"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>Status API</b><span>The same rows as JSON, for scripts and monitors.</span><span class="go">openvibe.network/api/v1/status →</span></a>
++ <a class="card" href="https://openvibe.network/api/v1/registry/releases"><div class="ic"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></div><b>Releases</b><span>The release each service runs, and how far behind the shared libraries it is.</span><span class="go">openvibe.network/api/v1/registry/releases →</span></a>
++ </div>
++ </section>
++
++ <section class="live" aria-live="polite">
++ <h2>Right now</h2>
++ <p class="lead2">Read from <a href="https://openvibe.network/api/v1/registry/health" style="color:inherit">openvibe.network/api/v1/registry/health</a> when this page opened.</p>
++ <p class="sum" id="live-sum">Open the <a href="https://openvibe.network/status" style="color:inherit">status page</a> for the current state of every service.</p>
++ <div class="tbl" id="live-tbl" hidden><table><thead><tr><th>Service</th><th>Status</th><th>Where it runs</th></tr></thead><tbody id="live-rows"></tbody></table></div>
++ </section>
++<script>
++(function () {
++ var sum = document.getElementById('live-sum'), tbl = document.getElementById('live-tbl'), rows = document.getElementById('live-rows');
++ if (!sum || !window.fetch) return;
++ var WHERE = { live: 'public', internal: 'loopback only, no public site yet', library: 'library (nothing to run)', repository: 'repository (nothing running)', placeholder: 'planned (nothing running)' };
++ function el(tag, text, cls) { var n = document.createElement(tag); if (text != null) n.textContent = String(text); if (cls) n.className = cls; return n; }
++ fetch('https://openvibe.network/api/v1/registry/health', { headers: { accept: 'application/json' } }).then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }).then(function (d) {
++ var s = d.summary || {}, order = ['up', 'degraded', 'down', 'not-running', 'unknown'], parts = [];
++ sum.textContent = '';
++ order.forEach(function (k) { if (s[k] == null) return; var b = el('b', s[k]); var w = document.createDocumentFragment(); w.appendChild(b); w.appendChild(document.createTextNode(' ' + k.replace('-', ' '))); parts.push(w); });
++ parts.forEach(function (p, i) { if (i) sum.appendChild(document.createTextNode(' · ')); sum.appendChild(p); });
++ var at = d.checked_at;
++ if (at) sum.appendChild(document.createTextNode(' · checked ' + new Date(at).toUTCString().replace(/ GMT$/, ' UTC')));
++ (d.services || []).forEach(function (x) {
++ var tr = document.createElement('tr');
++ var st = String(x.status || 'unknown');
++ tr.appendChild(el('td', x.id));
++ tr.appendChild(el('td', (st === 'up' && x.state !== 'live' ? 'up (loopback only)' : st.replace('-', ' ')) + (x.stale ? ' (stale)' : ''), 'st-' + st));
++ tr.appendChild(el('td', WHERE[x.state] || x.state || 'unknown'));
++ rows.appendChild(tr);
++ });
++ if (rows.children.length) tbl.hidden = false;
++ }).catch(function () { sum.textContent = 'The status API did not answer just now. Try the status page itself: https://openvibe.network/status'; });
++})();
++</script>
++
++ <section class="net">
++ <h2>The OpenVibe network</h2>
++ <p class="lead2">One account for all of it. Sign in once and every site signs you in.</p>
++ <div class="grid">
++ <a class="card" href="https://openvibe.live"><div class="ic"><i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i></div><b>OpenVibe.Live</b><span>Go live, chat, clip, restream</span><span class="go">openvibe.live →</span></a>
++ <a class="card" href="https://openvibe.tools"><div class="ic"><i class="fa-solid fa-screwdriver-wrench" aria-hidden="true"></i></div><b>OpenVibe.Tools</b><span>Hundreds of online tools</span><span class="go">openvibe.tools →</span></a>
++ <a class="card" href="https://openvibe.community"><div class="ic"><i class="fa-solid fa-people-group" aria-hidden="true"></i></div><b>OpenVibe.Community</b><span>Pastes, posts, people</span><span class="go">openvibe.community →</span></a>
++ <a class="card" href="https://openvibe.games"><div class="ic"><i class="fa-solid fa-gamepad" aria-hidden="true"></i></div><b>OpenVibe.Games</b><span>Scraplandia and browser games</span><span class="go">openvibe.games →</span></a>
++ <a class="card" href="https://openvibe.media"><div class="ic"><i class="fa-solid fa-photo-film" aria-hidden="true"></i></div><b>OpenVibe.Media</b><span>VODs, clips and files</span><span class="go">openvibe.media →</span></a>
++ <a class="card" href="https://openvibe.network"><div class="ic"><i class="fa-solid fa-circle-nodes" aria-hidden="true"></i></div><b>OpenVibe.Network</b><span>One account, every site</span><span class="go">openvibe.network →</span></a>
++ </div>
++ <h2 style="margin-top:28px;font-size:16px">Other OpenVibe domains, not open yet</h2>
++ <div class="rooms" style="margin-top:8px">
++ <a href="https://openvibe.news/"><i class="fa-solid fa-newspaper" aria-hidden="true"></i>OpenVibe.News</a>
++ <a href="https://openvibe.reviews/"><i class="fa-solid fa-star-half-stroke" aria-hidden="true"></i>OpenVibe.Reviews</a>
++ <a href="https://openvibe.tips/"><i class="fa-solid fa-hand-holding-heart" aria-hidden="true"></i>OpenVibe.Tips</a>
++ <a href="https://openvibe.vip/"><i class="fa-solid fa-crown" aria-hidden="true"></i>OpenVibe.VIP</a>
++ <a href="https://openvibe.trade/"><i class="fa-solid fa-chart-line" aria-hidden="true"></i>OpenVibe.Trade</a>
++ <a href="https://openvibe.host/"><i class="fa-solid fa-server" aria-hidden="true"></i>OpenVibe.Host</a>
++ <a href="https://openvibe.deals/"><i class="fa-solid fa-tags" aria-hidden="true"></i>OpenVibe.Deals</a>
++ <a href="https://openvibe.coupons/"><i class="fa-solid fa-ticket" aria-hidden="true"></i>OpenVibe.Coupons</a>
++ <a href="https://openre.stream/"><i class="fa-solid fa-satellite-dish" aria-hidden="true"></i>OpenRe.Stream</a>
++ <a href="https://auth.openvibe.network/"><i class="fa-solid fa-key" aria-hidden="true"></i>auth.openvibe.network</a>
++ <a href="https://api.openvibe.network/"><i class="fa-solid fa-plug" aria-hidden="true"></i>api.openvibe.network</a>
++ <a href="https://admin.openvibe.network/"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i>admin.openvibe.network</a>
++ <a href="https://themes.openvibe.network/"><i class="fa-solid fa-palette" aria-hidden="true"></i>themes.openvibe.network</a>
++ <a href="https://ai.openvibe.services/"><i class="fa-solid fa-robot" aria-hidden="true"></i>ai.openvibe.services</a>
++ <a href="https://openvibe.pics/"><i class="fa-solid fa-images" aria-hidden="true"></i>OpenVibe.Pics</a>
++ <a href="https://openvibe.download/"><i class="fa-solid fa-cloud-arrow-down" aria-hidden="true"></i>OpenVibe.Download</a>
++ <a href="https://openvibe.space/"><i class="fa-solid fa-layer-group" aria-hidden="true"></i>OpenVibe.Space</a>
++ <a href="https://openvibe.food/"><i class="fa-solid fa-utensils" aria-hidden="true"></i>OpenVibe.Food</a>
++ <a href="https://openvibe.quest/"><i class="fa-solid fa-map" aria-hidden="true"></i>OpenVibe.Quest</a>
++ <a href="https://openvibe.rent/"><i class="fa-solid fa-key" aria-hidden="true"></i>OpenVibe.Rent</a>
++ <a href="https://openvibe.homes/"><i class="fa-solid fa-house" aria-hidden="true"></i>OpenVibe.Homes</a>
++ <a href="https://openvibe.services/"><i class="fa-solid fa-server" aria-hidden="true"></i>OpenVibe.Services</a>
++ <a href="https://openvibe.run/"><i class="fa-solid fa-terminal" aria-hidden="true"></i>OpenVibe.Run</a>
++ <a href="https://openvibe.work/"><i class="fa-solid fa-briefcase" aria-hidden="true"></i>OpenVibe.Work</a>
++ <a href="https://openvibe.zone/"><i class="fa-solid fa-box-archive" aria-hidden="true"></i>OpenVibe.Zone</a>
++ <a href="https://openvibe.video/"><i class="fa-solid fa-film" aria-hidden="true"></i>OpenVibe.Video</a>
++ <a href="https://openvibe.website/"><i class="fa-solid fa-globe" aria-hidden="true"></i>OpenVibe.Website</a>
++ <a href="https://openvibe.help/"><i class="fa-solid fa-life-ring" aria-hidden="true"></i>OpenVibe.Help</a>
++ <a href="https://openvibe.bot/"><i class="fa-solid fa-robot" aria-hidden="true"></i>OpenVibe.Bot</a>
++ <a href="https://openvibe.watch/"><i class="fa-solid fa-eye" aria-hidden="true"></i>OpenVibe.Watch</a>
++ <a href="https://openvibe.actor/"><i class="fa-solid fa-user-astronaut" aria-hidden="true"></i>OpenVibe.Actor</a>
++ <a href="https://openvibe.events/"><i class="fa-solid fa-bolt" aria-hidden="true"></i>OpenVibe.Events</a>
++ </div>
++ </section>
++
++ <div class="strip">
++ <span class="ov-mark" data-size="44" data-variant="status"></span>
++ <h3>Open source. Community run.</h3>
++ <p>Every OpenVibe site is built in the open by the people using it — free speech within the rules, no gatekeeping, one identity that follows you everywhere.</p>
++ <a class="btn" href="https://github.com/OpenVibers" rel="noopener"><i class="fa-brands fa-github"></i> OpenVibers on GitHub</a>
++ </div>
++</main>
++<style id="ovf-css">
++.ovf{background:var(--bg-secondary,#12131c);border-top:1px solid var(--border,rgba(255,255,255,.08));margin-top:40px;
++ padding:clamp(24px,4vw,40px) clamp(14px,4vw,32px) calc(clamp(20px,3vw,28px) + env(safe-area-inset-bottom,0px));}
++.ovf[data-variant="compact"]{margin-top:24px;padding:14px clamp(12px,3vw,24px) calc(14px + env(safe-area-inset-bottom,0px));}
++.ovf-inner{max-width:1240px;margin:0 auto}
++.ovf-inner--compact{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px 18px;text-align:center}
++.ovf-cols{display:grid;gap:clamp(20px,3vw,36px);grid-template-columns:minmax(230px,1.6fr) repeat(auto-fit,minmax(120px,1fr));align-items:start}
++.ovf-col{display:flex;flex-direction:column;gap:8px;min-width:0}
++.ovf-col h3{margin:0 0 2px;font-size:.72rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted,#8b93ad)}
++.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}
++.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
++.ovf-more{font-weight:600;opacity:.85}.ovf-ic{flex:none;vertical-align:-.14em}.ovf-more .ovf-ic{font-size:.85em;margin-left:3px;transition:transform .15s}.ovf-more:hover .ovf-ic{transform:translateX(3px)}.ovf-social .ovf-ic{font-size:17px}.ovf-account-links .ovf-ic{font-size:1.05em;opacity:.85}
++.ovf-brand{display:inline-flex;align-items:center;gap:9px;color:var(--text-primary,#f1f4fb);font-size:1.05rem;font-weight:700;text-decoration:none}
++.ovf-brand--sm{font-size:.9rem}
++.ovf-tagline{margin:0;font-size:.82rem;line-height:1.55;color:var(--text-muted,#8b93ad);max-width:46ch}
++.ovf-social{display:flex;gap:8px;margin-top:4px}
++.ovf-social a{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--text-secondary,#b6bdd2);
++ background:var(--bg-tertiary,#1a1b28);border:1px solid var(--border,rgba(255,255,255,.08));font-size:.95rem;transition:color .15s,border-color .15s,transform .15s}
++.ovf-social a:hover{color:var(--accent,#60a5fa);border-color:var(--accent,#60a5fa);transform:translateY(-2px)}
++.ovf-account{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 18px;
++ margin-top:clamp(18px,2.5vw,26px);padding:10px 14px;border-radius:14px;
++ background:color-mix(in srgb, var(--accent,#60a5fa) 8%, var(--bg-tertiary,#1a1b28));
++ border:1px solid color-mix(in srgb, var(--accent,#60a5fa) 26%, var(--border,rgba(255,255,255,.08)));
++ animation:ovfIn .4s ease-out both}
++@keyframes ovfIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
++.ovf-account-who{display:inline-flex;align-items:center;gap:8px;font-size:.84rem;color:var(--text-secondary,#b6bdd2)}
++.ovf-account-who strong{color:var(--text-primary,#f1f4fb)}
++.ovf-avatar{width:24px;height:24px;border-radius:50%;overflow:hidden;display:grid;place-items:center;
++ background:var(--accent,#60a5fa);color:#fff;font-size:.7rem;font-weight:800;flex:none}
++.ovf-avatar img{width:100%;height:100%;object-fit:cover}
++.ovf-account-links{display:flex;flex-wrap:wrap;gap:6px}
++.ovf-account-links a{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;font-size:.78rem;font-weight:600;
++ color:var(--text-secondary,#b6bdd2);background:var(--bg-secondary,#12131c);border:1px solid var(--border,rgba(255,255,255,.08));
++ text-decoration:none;transition:color .15s,border-color .15s,transform .15s}
++.ovf-account-links a:hover{color:var(--accent,#60a5fa);border-color:var(--accent,#60a5fa);transform:translateY(-1px)}
++.ovf-account-links i{font-size:.72rem;opacity:.8}
++.ovf-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;
++ margin-top:clamp(20px,3vw,30px);padding-top:16px;border-top:1px solid var(--border,rgba(255,255,255,.08));
++ padding-inline:clamp(0px, calc(84px - (100vw - 1240px) / 2), 84px)}
++.ovf-copy{color:var(--text-muted,#8b93ad);font-size:.78rem}
++.ovf-legal-inline,.ovf-compact-links{display:flex;flex-wrap:wrap;gap:14px}
++.ovf [hidden],.ovf-account[hidden]{display:none!important}
++.ovf .ovf-shipped.ov-shipped-latest{font-size:.76rem;padding:4px 12px;max-width:min(520px,100%);background:transparent}
++.ovf-legal-inline a,.ovf-compact-links a{color:var(--text-muted,#8b93ad);font-size:.78rem;text-decoration:none;transition:color .15s}
++.ovf-legal-inline a:hover,.ovf-compact-links a:hover{color:var(--accent,#60a5fa)}
++.ovf-compact-meta{color:var(--text-muted,#8b93ad);font-size:.74rem}
++@media (max-width:900px){.ovf-cols{grid-template-columns:repeat(2,minmax(0,1fr))}.ovf-col--brand{grid-column:1/-1}}
++@media (max-width:560px){
++ .ovf{padding-bottom:calc(20px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
++ .ovf[data-variant="compact"]{padding-bottom:calc(14px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
++ .ovf-cols{gap:22px 16px}
++ .ovf-bar{justify-content:center;text-align:center;padding-inline:0}
++ .ovf-legal-inline{justify-content:center}
++ .ovf-account{flex-direction:column;align-items:flex-start}
++}
++@media (prefers-reduced-motion:reduce){.ovf-account{animation:none}.ovf-col a:hover,.ovf-social a:hover,.ovf-account-links a:hover{transform:none}}
++.ovf{-webkit-tap-highlight-color:transparent}
++:where(.ovf) :where(a,button):active{scale:.97}
++:where(.ovf) :where(a,button):focus-visible{outline:2px solid var(--accent-light,var(--accent,#3b82f6));outline-offset:2px;border-radius:4px}
++@media (prefers-reduced-motion:reduce){:where(.ovf) :where(a,button):active{scale:none;opacity:.82}}</style><footer id="ov-footer" class="ovf" data-variant="full"><div class="ovf-inner">
++ <div class="ovf-cols">
++ <div class="ovf-col ovf-col--brand">
++ <a class="ovf-brand" href="/"><span class="ov-mark" data-size="26"></span><span>OpenVibe</span></a>
++ <p class="ovf-tagline">Open source and community run. Built in the open by the people using it.</p>
++ <div class="ovf-social">
++ <a href="https://github.com/OpenVibers" target="_blank" rel="noopener" aria-label="GitHub" title="GitHub"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path fill="currentColor" stroke="none" d="M12 2.2a9.8 9.8 0 0 0-3.1 19.1c.5.1.7-.2.7-.5v-1.7c-2.7.6-3.3-1.2-3.3-1.2-.4-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.2-.2-4.5-1.1-4.5-4.8 0-1.1.4-1.9 1-2.6-.1-.2-.4-1.2.1-2.6 0 0 .8-.3 2.7 1a9.3 9.3 0 0 1 4.9 0c1.9-1.3 2.7-1 2.7-1 .5 1.4.2 2.4.1 2.6.6.7 1 1.5 1 2.6 0 3.7-2.3 4.6-4.5 4.8.4.3.7.9.7 1.800v2.700c0 .3.2.6.7.5A9.8 9.8 0 0 0 12 2.200z"/></svg></a><a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener" aria-label="Discord" title="Discord"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path fill="currentColor" stroke="none" d="M19.3 5.300a16.5 16.5 0 0 0-4.1-1.300l-.5 1a15.3 15.3 0 0 0-5.4 0l-.5-1a16.5 16.5 0 0 0-4.1 1.300C2.1 9.2 1.4 13 1.7 16.700a16.6 16.6 0 0 0 5.1 2.600l1.1-1.800c-.6-.2-1.2-.5-1.7-.8l.4-.3a11.8 11.8 0 0 0 10.8 0l.4.3c-.5.3-1.1.6-1.7.8l1.1 1.800a16.6 16.6 0 0 0 5.1-2.600c.4-4.3-.7-8.1-3-11.400zM8.7 14.400c-1 0-1.8-.9-1.8-2.100s.8-2.1 1.8-2.1 1.9 1 1.8 2.100c0 1.2-.8 2.1-1.8 2.100zm6.6 0c-1 0-1.8-.9-1.8-2.100s.8-2.1 1.8-2.1 1.9 1 1.8 2.100c0 1.2-.8 2.1-1.8 2.100z"/></svg></a>
++ <a href="/sitemap.xml" aria-label="Sitemap" title="Sitemap"><svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="3.5" width="6" height="4.5" rx="1"/><rect x="3" y="16" width="6" height="4.5" rx="1"/><rect x="15" y="16" width="6" height="4.5" rx="1"/><path d="M12 8v4M6 16v-2.500h12V16"/></svg></a>
++ </div>
++ </div>
++
++ <nav class="ovf-col" aria-label="The OpenVibe network">
++ <h3>Network</h3>
++ <a href="https://openvibe.live" title="Go live, restream and clip">Live streaming</a><a href="https://openvibe.tools" title="The whole toolbox">Online tools</a><a href="https://dev.openvibe.tools" title="Formatters, encoders, validators">Developer tools</a><a href="https://net.openvibe.tools" title="Ping, DNS, Whois, SSL">Network diagnostics</a><a href="https://img.openvibe.tools" title="Convert, resize, compress">Image converter</a><a href="https://audio.openvibe.tools" title="Convert, trim, pitch-shift">Audio converter</a>
++ <a class="ovf-more" href="https://openvibe.network" target="_blank" rel="noopener">Everything else <svg class="ovf-ic" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.800" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
++ </nav>
++
++ <nav class="ovf-col" aria-label="Legal">
++ <h3>Legal</h3>
++ <a href="https://openvibe.live/tos">Terms of Service</a><a href="https://openvibe.live/privacy">Privacy Policy</a><a href="https://openvibe.live/dmca">DMCA</a>
++ </nav>
++ </div>
++ <div class="ovf-account" id="ovf-account" hidden></div>
++ <div class="ovf-bar">
++ <span class="ovf-copy">Open source &amp; community driven · built in the open</span>
++ <a class="ovf-shipped" data-ov-shipped="latest" data-service="status" href="https://openvibe.network/updates?site=status" hidden></a>
++ <span class="ovf-legal-inline">
++ <a href="https://openvibe.network/updates?site=status">Updates</a>
++ <a href="https://openvibe.live/tos">Terms</a><a href="https://openvibe.live/privacy">Privacy</a><a href="https://openvibe.live/dmca">DMCA</a>
++ </span>
++ </div>
++ </div></footer>
++<script src="/shared/ov-mark.js?v=03f3596adcab" async></script>
++<script src="/shared/navbar.js?v=017a740747b4"></script>
++<script src="/shared/footer.js?v=a85ab893be88"></script>
++<script>
++(function () {
++ if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'status', apiBase: 'https://openvibe.network', links: [{ label: 'Where to go', href: '#what' }, { label: 'The network', href: 'https://openvibe.network/#network', external: false }], history: { type: 'page', title: 'status.openvibe.network' } }); } catch (e) {} }
++ if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: 'status', variant: 'full', links: [{ heading: 'status.openvibe.network', items: [{ name: 'Sign in', url: 'https://openvibe.network/login?return=https%3A%2F%2Fstatus.openvibe.network%2F' }, { name: 'The network', url: 'https://openvibe.network/#network' }, { name: 'Discord', url: 'https://discord.gg/M6MuRUaeJj' }] }] }); } catch (e) {} }
++})();
++</script>
++</body>
++</html>
ddiff --git a/notices.json b/notices.json
nnew file mode 100644
iindex 0000000..eb58817
---- /dev/null
++++ b/notices.json
@@@ -0,0 +1,70 @@
++[
++ {
++ "domain": "ai.openvibe.network",
++ "tld": "ai",
++ "name": "ai.openvibe.network",
++ "tagline": "Moved: OpenVibe.AI's public home is ai.openvibe.services.",
++ "description": "OpenVibe.AI's public address is now ai.openvibe.services, next to the rest of the network's programmable services on OpenVibe.Services. This address serves only this notice until every client has moved; then it redirects.",
++ "accent": "#c084fc",
++ "zone": "openvibe.network",
++ "legalProfile": "info",
++ "kind": "moved",
++ "links": [
++ {
++ "label": "ai.openvibe.services",
++ "href": "https://ai.openvibe.services/"
++ },
++ {
++ "label": "OpenVibe.Services",
++ "href": "https://openvibe.services/"
++ }
++ ]
++ },
++ {
++ "domain": "realtime.openvibe.network",
++ "tld": "realtime",
++ "name": "realtime.openvibe.network",
++ "tagline": "Closed: realtime delivery is part of OpenVibe.Events.",
++ "description": "OpenVibe.Realtime was planned as a separate service for live updates in the browser. Decision record ADR-005 (accepted 2026-09-22) closed it: browser realtime is part of OpenVibe.Events, as server-sent events with cursor resume at events.openvibe.network/realtime/stream. This address serves only this notice.",
++ "accent": "#34d399",
++ "zone": "openvibe.network",
++ "legalProfile": "info",
++ "kind": "closed",
++ "links": [
++ {
++ "label": "OpenVibe.Events",
++ "href": "https://events.openvibe.network/"
++ },
++ {
++ "label": "ADR-005: realtime",
++ "href": "https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-005-realtime.md"
++ }
++ ]
++ },
++ {
++ "domain": "status.openvibe.network",
++ "tld": "status",
++ "name": "status.openvibe.network",
++ "tagline": "Is OpenVibe up?",
++ "description": "OpenVibe.Network checks every OpenVibe service about once a minute (its readiness endpoint and the release it runs) and publishes what it saw at openvibe.network/status. This address points there.",
++ "accent": "#22c55e",
++ "zone": "openvibe.network",
++ "legalProfile": "info",
++ "statusApi": "https://openvibe.network/api/v1/registry/health",
++ "kind": "status",
++ "links": [
++ {
++ "label": "Status page",
++ "href": "https://openvibe.network/status"
++ },
++ {
++ "label": "Status API",
++ "href": "https://openvibe.network/api/v1/status"
++ },
++ {
++ "label": "Releases",
++ "href": "https://openvibe.network/api/v1/registry/releases"
++ }
++ ]
++ }
++]
ddiff --git a/package.json b/package.json
iindex c9951d2..1424ba2 100644
---- a/package.json
++++ b/package.json
@@@ -1,7 +1,7 @@
{
"name": "openvibe-sites",
"private": true,
-- "description": "Placeholder and hub pages for the planned OpenVibe domains (static build)",
++ "description": "Static notices and frozen product pages for OpenVibe domains",
"scripts": {
"build": "node build.js",
"test": "node test/run.js"
ddiff --git a/scripts/facts.js b/scripts/facts.js
iindex ecc1a36..89bf8fd 100644
---- a/scripts/facts.js
++++ b/scripts/facts.js
@@@ -1,97 +1,5 @@
#!/usr/bin/env node
'use strict';
--/**
-- * Refresh facts.json: what is true about each repository a Sites page names, so the placeholder
-- * copy comes from facts and never from a hand-kept list (roadmap §32.7 launch rule, §32.10).
-- *
-- * node scripts/facts.js → STATUS.json of every repo + Network's registry exposure
-- * node scripts/facts.js --offline → STATUS.json only; keeps the registry rows already in facts.json
-- *
-- * Two sources, both recorded with where they came from:
-- * - each repository's committed STATUS.json (`git show HEAD:STATUS.json` in the sibling OpenVibers
-- * checkout, so nobody's uncommitted edit leaks in), with the commit it was read at;
-- * - Network's registry exposure (GET /api/v1/registry/services): whether each service's public
-- * domain serves it (live), it runs on the host's loopback only (internal), or nothing runs.
-- *
-- * build.js reads only the committed facts.json, so a build is reproducible (CI has no sibling
-- * checkouts and no network). Review the diff, rebuild (node build.js), commit both.
-- */
--const fs = require('fs');
--const path = require('path');
--const { execFileSync } = require('child_process');
--
--const ROOT = path.join(__dirname, '..');
--const REPOS_DIR = process.env.OPENVIBERS_DIR || path.join(ROOT, '..');
--const REGISTRY = process.env.REGISTRY_URL || 'https://openvibe.network/api/v1/registry/services';
--const OUT = path.join(ROOT, 'facts.json');
--const offline = process.argv.includes('--offline');
--
--const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, 'sites.json'), 'utf8'));
--const previous = fs.existsSync(OUT) ? JSON.parse(fs.readFileSync(OUT, 'utf8')) : { repos: {} };
--
--// A STATUS.json says "deployed" as a boolean, as prose ("loopback only, not launched") or only in
--// its "deployment" sentence ("Deployed internally, not launched: …"; "none of its own: …").
--function deployedOf(s) {
-- const says = (v) => !/^(no|not|none)\b/i.test(v.trim());
-- if (typeof s.deployed === 'boolean') return s.deployed;
-- if (typeof s.deployed === 'string') return says(s.deployed);
-- if (typeof s.deployment === 'string') return says(s.deployment);
-- return null;
--}
--
--function statusOf(repo) {
-- const dir = path.join(REPOS_DIR, repo);
-- if (!fs.existsSync(path.join(dir, '.git'))) return { missing: `no checkout at ${dir}` };
-- let raw;
-- try { raw = execFileSync('git', ['-C', dir, 'show', 'HEAD:STATUS.json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }); } catch { return null; }
-- const s = JSON.parse(raw);
-- const commit = execFileSync('git', ['-C', dir, 'rev-parse', '--short=7', 'HEAD'], { encoding: 'utf8' }).trim();
-- return { stage: s.stage || null, code: s.code === true, deployed: deployedOf(s), updated: s.updated || null, commit };
--}
--
--async function registryRows() {
-- const res = await fetch(REGISTRY, { headers: { accept: 'application/json' } });
-- if (!res.ok) throw new Error(`${REGISTRY} answered ${res.status}`);
-- const body = await res.json();
-- const byRepo = {};
-- for (const m of body.services) {
-- const repo = String(m.repository || '').replace(/^OpenVibers\//, '');
-- if (!repo) continue;
-- const e = m.exposure || {};
-- const origin = e.state === 'live' && e.public_site === 'service' ? m.publicOrigin || null : null;
-- byRepo[repo] = { id: m.id, manifest_status: m.status, state: e.state || 'unknown', public_site: e.public_site == null ? null : e.public_site, origin, planned_origin: origin ? null : m.publicOrigin || null };
-- }
-- return { contracts_version: body.contracts_version || null, byRepo };
--}
--
--(async () => {
-- const repos = [...new Set(catalog.sites.map(s => s.plannedRepo).filter(Boolean))].sort();
-- const reg = offline ? null : await registryRows();
-- const out = {
-- note: 'Facts the Sites placeholder copy is built from. Refresh with `node scripts/facts.js`, review the diff, run `node build.js`, commit both. Never edit by hand.',
-- generated: new Date().toISOString().slice(0, 10),
-- sources: {
-- status: 'STATUS.json at HEAD of each OpenVibers checkout (commit recorded per repo)',
-- registry: offline ? (previous.sources && previous.sources.registry) || null : `${REGISTRY} (openvibe-contracts ${reg.contracts_version})`,
-- },
-- repos: {},
-- };
-- for (const repo of repos) {
-- const status = statusOf(repo);
-- if (status && status.missing) {
-- // No checkout here: keep what the last refresh saw rather than inventing anything.
-- if (!previous.repos[repo]) throw new Error(`${repo}: ${status.missing} and no earlier fact to keep`);
-- console.warn(`[facts] ${repo}: ${status.missing}; kept the previous STATUS row`);
-- out.repos[repo] = { ...previous.repos[repo] };
-- } else {
-- out.repos[repo] = { status };
-- }
-- const registry = offline ? (previous.repos[repo] || {}).registry || null : reg.byRepo[repo] || null;
-- out.repos[repo].registry = registry;
-- }
-- fs.writeFileSync(OUT, JSON.stringify(out, null, 2) + '\n');
-- for (const [repo, f] of Object.entries(out.repos)) {
-- const s = f.status || {}; const r = f.registry || {};
-- console.log(`${repo.padEnd(18)} stage=${s.stage} code=${s.code} deployed=${s.deployed} registry=${r.state || '-'}${r.public_site ? `/${r.public_site}` : ''} @${s.commit || '-'}`);
-- }
--})().catch((e) => { console.error(`[facts] ${e.message}`); process.exit(1); });
++// The former repository snapshot refresh is retired with the product generator.
++console.error('Repository snapshot refresh is retired; notice builds use notices.json.');
++process.exitCode = 1;
ddiff --git a/test/build.test.js b/test/build.test.js
iindex d0d463b..df5bb7b 100644
---- a/test/build.test.js
++++ b/test/build.test.js
@@@ -1,165 +1,64 @@
'use strict';
--// OpenVibe.Sites: the built placeholders are current, complete and honest.
--// node test/build.test.js
++// The three notices are current, while product pages stay frozen.
const assert = require('assert');
const fs = require('fs');
const path = require('path');
--const { execFileSync } = require('child_process');
const ROOT = path.join(__dirname, '..');
--const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, 'sites.json'), 'utf8'));
++const notices = require('../notices.json');
++const frozen = require('../frozen.json');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
++const exists = (p) => fs.existsSync(path.join(ROOT, p));
--// dist/ and the vhosts must match sites.json + build.js (deploy serves dist/ as committed).
--execFileSync(process.execPath, [path.join(ROOT, 'build.js'), '--check'], { stdio: 'pipe' });
++assert.strictEqual(require('../build.js').check(), 0, 'generated notice files are current');
++assert.doesNotMatch(read('build.js'), /repoFacts|visionViz|facts\.json|sites\.json/, 'builder has no product generator dependencies');
++assert.doesNotMatch(read('scripts/facts.js'), /facts\.json|sites\.json/, 'retired snapshot script has no catalog inputs');
--// package.json keeps the shared runner shape (`node test/run.js`, what every other repository here uses):
--// a bare "npm test" of another shape is refused inside agent jobs by openvibe-agents' PATH shim, so
--// `ov test` could not run the suite at all (review of OpenVibers/openvibe-agents#51).
--const pkg = JSON.parse(read('package.json'));
--assert.strictEqual(pkg.scripts.test, 'node test/run.js', 'package.json: test runs node test/run.js');
--assert.ok(fs.existsSync(path.join(ROOT, 'test', 'run.js')), 'test/run.js exists (the shared runner)');
--
--// Pricing copy is never used on the network (no "free"/"$0"/"no ads" claims); "free speech" is fine.
--const PRICING = /\$0\b|\bfor free\b|\bfree (forever|to use|of charge|plan|tier)\b|\bno ads\b|\bad-free\b/i;
--// Claims no OpenVibe service makes good on (2026-09-24 audit): Host serves static files only (Stage C,
--// running user code, is not started) and handles neither DNS nor logs for anyone.
--const UNBACKED = /handled for you|host your (bot|mod)|bots,? (and|&amp;|&) mods hosting|\bservice hosting\b/i;
--
--let pages = 0;
--for (const site of catalog.sites) {
-- const d = site.domain;
-- const html = read(`dist/${d}/index.html`);
-- pages++;
-- assert.match(html, /<title>[^<]+<\/title>/, `${d}: title`);
-- assert.match(html, new RegExp(`<link rel="canonical" href="https://${d.replace(/\./g, '\\.')}/?"`), `${d}: canonical`);
-- // The exact phrase other repositories' checks grep for (Host docs/launch.md, Network's requirement ledger).
-- if (!site.kind) assert.ok(html.includes('this page is a placeholder, nothing here is live yet'), `${d}: says it is a placeholder (never claim a product is live)`);
-- else assert.ok(!html.includes('nothing here is live yet'), `${d}: a notice is not a placeholder`);
-- assert.doesNotMatch(html, PRICING, `${d}: pricing copy`);
-- assert.doesNotMatch(html, UNBACKED, `${d}: claims a hosting product that does not exist`);
-- for (const m of html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)) {
-- assert.doesNotThrow(() => JSON.parse(m[1]), `${d}: JSON-LD parses`);
-- }
-- for (const f of ['robots.txt', 'manifest.webmanifest', 'status.json', ...(site.kind ? [] : ['sitemap.xml'])]) {
-- assert.ok(fs.existsSync(path.join(ROOT, 'dist', d, f)), `${d}: ${f}`);
-- }
-- // The shared release watcher asks every page for /release.json; a placeholder publishes one, and its
-- // footer carries its own service's "shipped" line (the OpenVibe Frame).
-- const rel = JSON.parse(read(`dist/${d}/release.json`));
-- assert.strictEqual(rel.service, site.tld, `${d}: release.json service`);
-- assert.match(rel.release, /^[0-9a-f]{12}$/, `${d}: release.json release`);
-- if (!site.vhostOwner) assert.match(read(`deploy/nginx/${d}.conf`), /location = \/release\.json \{/, `${d}: nginx serves /release.json`);
-- // D42: the frame from this repository's pin (dist/_shared/, aliased at /shared/), never openvibe.network's.
-- assert.doesNotMatch(html, /https:\/\/openvibe\.network\/shared\/[\w.-]+\.js/, `${d}: loads a shared file from openvibe.network`);
-- for (const m of html.matchAll(/src="\/shared\/([\w.-]+\.js)\?v=([0-9a-f]{12})"/g)) {
-- assert.strictEqual(m[2], require('openvibe-shared/serve').hashOf(m[1]), `${d}: /shared/${m[1]} carries its content hash`);
-- assert.strictEqual(read(`dist/_shared/${m[1]}`), fs.readFileSync(require('openvibe-shared/files').path(m[1]), 'utf8'), `${d}: dist/_shared/${m[1]} is the pinned file`);
-- }
-- assert.match(html, /src="\/shared\/navbar\.js\?v=/, `${d}: the navbar from /shared`);
-- if (!site.vhostOwner) assert.match(read(`deploy/nginx/${d}.conf`), /location \^~ \/shared\/ \{\s+alias \/opt\/openvibe\.sites\/dist\/_shared\/;/, `${d}: nginx serves /shared/ from dist/_shared/`);
-- assert.ok(html.includes(`data-ov-shipped="latest" data-service="${site.tld}"`), `${d}: the footer's shipped line`);
-- if (site.kind) {
-- // A notice (a closed product, a pointer) is not indexed, has no sitemap and is not listed as opening.
-- assert.match(html, /<meta name="robots" content="noindex, follow">/, `${d}: notice is noindex`);
-- assert.ok(!fs.existsSync(path.join(ROOT, 'dist', d, 'sitemap.xml')), `${d}: notice has no sitemap`);
-- assert.doesNotMatch(read(`dist/${d}/robots.txt`), /Sitemap:/, `${d}: robots.txt names no sitemap`);
-- assert.ok(Array.isArray(site.links) && site.links.length, `${d}: notice links where to go`);
-- for (const [, href] of site.links) assert.ok(html.includes(`href="${href}"`), `${d}: links ${href}`);
-- for (const other of catalog.sites) {
-- if (other.domain !== d) assert.ok(!read(`dist/${other.domain}/index.html`).includes(`href="https://${d}/"`), `${other.domain}: does not list the ${site.kind} ${d} as a domain still to open`);
-- }
-- }
-- if (site.kind === 'status') {
-- // status.openvibe.network points at what Network publishes; it never invents a status of its own.
-- assert.ok(site.statusApi && html.includes(`fetch('${site.statusApi}'`), `${d}: reads the live status API`);
-- assert.match(html, /Status: <b>status<\/b>/, `${d}: labelled as a status pointer`);
-- assert.doesNotMatch(html, /innerHTML/, `${d}: builds the live rows with DOM nodes`);
-- assert.strictEqual(JSON.parse(read(`dist/${d}/status.json`)).repoStage, 'pointer', `${d}: status.json repoStage`);
-- }
-- if (site.kind === 'closed') {
-- assert.match(html, /Status: <b>closed<\/b>/, `${d}: says closed`);
-- assert.doesNotMatch(html, /will be|opening soon|coming soon/i, `${d}: a closed product is never described as coming`);
-- }
-- // Unknown paths are real 404s with a page (no soft 404: the front page is never the fallback).
-- const nf = read(`dist/${d}/404.html`);
-- assert.match(nf, /<meta name="robots" content="noindex">/, `${d}: 404 page is noindex`);
-- assert.ok(nf.includes(`href="https://${d}/"`), `${d}: 404 page links the front page`);
-- // A site whose service repository owns the vhost (vhostOwner) has its nginx config built there, not here.
-- if (!site.vhostOwner) {
-- const conf = read(`deploy/nginx/${d}.conf`);
-- assert.ok(conf.includes(`server_name ${d}`), `${d}: vhost server_name`);
-- assert.ok(conf.includes(`root /opt/openvibe.sites/dist/${d};`), `${d}: vhost root`);
-- assert.ok(conf.includes('error_page 404 /404.html;'), `${d}: vhost answers 404 with /404.html`);
-- assert.ok(conf.includes('try_files $uri $uri.html $uri/ =404;'), `${d}: vhost try_files ends in =404`);
-- assert.doesNotMatch(conf, /try_files[^;]*\/index\.html;/, `${d}: vhost never falls back to the front page`);
-- // nginx drops the server-level add_header list in any location with its own add_header: each such
-- // location must repeat the security headers (they were missing from every Sites response until 2026-09-24).
-- for (const m of conf.matchAll(/location [^{]*\{((?:[^{}]|\{[^{}]*\})*)\}/g)) {
-- if (!/add_header/.test(m[1])) continue;
-- for (const h of ['X-Content-Type-Options', 'X-Frame-Options', 'Referrer-Policy', 'Strict-Transport-Security']) {
-- assert.ok(m[1].includes(`add_header ${h} `), `${d}: ${m[0].split('{')[0].trim()} repeats ${h}`);
-- }
-- }
-- }
++assert.deepStrictEqual(notices.map(n => n.domain), [
++ 'ai.openvibe.network', 'realtime.openvibe.network', 'status.openvibe.network',
++]);
++assert.strictEqual(new Set(frozen).size, frozen.length, 'frozen domains are unique');
++assert.strictEqual(frozen.length, 32, 'all 32 product domains are frozen');
++const active = new Set(notices.map(n => n.domain));
++for (const domain of frozen) {
++ assert.ok(!active.has(domain), `${domain}: product is not a notice`);
++ assert.ok(exists(`dist/${domain}/index.html`), `${domain}: frozen front page`);
++ if (domain !== 'openvibe.bot') assert.ok(exists(`deploy/nginx/${domain}.conf`), `${domain}: frozen vhost`);
}
--
--// openvibe.bot hands its vhost to OpenVibe.Bot (vhostOwner): no generated conf here, but Sites still
--// publishes every page for it.
--const bot = catalog.sites.find((s) => s.domain === 'openvibe.bot');
--assert.ok(bot && bot.vhostOwner, 'openvibe.bot: sites.json gives its vhost to another repository');
--assert.ok(!fs.existsSync(path.join(ROOT, 'deploy', 'nginx', 'openvibe.bot.conf')), 'openvibe.bot: no generated deploy/nginx/openvibe.bot.conf');
--for (const f of ['index.html', 'privacy.html', 'terms.html', 'dmca.html', '404.html']) {
-- assert.ok(fs.existsSync(path.join(ROOT, 'dist', 'openvibe.bot', f)), `openvibe.bot: ${f} is still built`);
++assert.ok(!exists('deploy/nginx/openvibe.bot.conf'), 'Bot owns its vhost');
++for (const domain of ['openvibe.actor', 'openvibe.bot']) {
++ assert.ok(frozen.includes(domain), `${domain}: remains frozen`);
++ assert.match(read(`dist/${domain}/index.html`), /<meta name="robots" content="index, follow, max-image-preview:large">/, `${domain}: original indexing`);
++ assert.strictEqual(JSON.parse(read(`dist/${domain}/status.json`)).stage, 'placeholder', `${domain}: lifecycle stage`);
++ assert.strictEqual(JSON.parse(read(`dist/${domain}/release.json`)).kind, 'placeholder', `${domain}: release kind`);
++ assert.ok(exists(`dist/${domain}/sitemap.xml`), `${domain}: frozen sitemap`);
++ assert.ok(read(`dist/${domain}/robots.txt`).includes(`Sitemap: https://${domain}/sitemap.xml`), `${domain}: sitemap advertised`);
}
--// Labels come from facts (facts.json: each repository's STATUS.json + the Network registry), never a
--// hand-kept list: a product whose code exists is never "charter only", and a product that runs says
--// what its public launch waits for (roadmap §32.7 launch rule, §32.10).
--const facts = JSON.parse(read('facts.json'));
--const NO_CODE = /charter only|no code yet/i;
--for (const site of catalog.sites) {
-- if (!site.plannedRepo) continue;
-- const d = site.domain;
-- const f = facts.repos[site.plannedRepo];
-- assert.ok(f, `${d}: facts.json has a row for ${site.plannedRepo} (node scripts/facts.js)`);
++for (const notice of notices) {
++ const d = notice.domain;
const html = read(`dist/${d}/index.html`);
-- const status = JSON.parse(read(`dist/${d}/status.json`));
-- const st = f.status || {}; const rg = f.registry || {};
-- if (st.stage === 'closed') assert.strictEqual(site.kind, 'closed', `${d}: ${site.plannedRepo} is closed (STATUS.json), so its page is a closed notice, not a placeholder`);
-- const hasCode = st.code === true || ['live', 'internal'].includes(rg.state);
-- if (hasCode) {
-- assert.doesNotMatch(html, NO_CODE, `${d}: ${site.plannedRepo} has code, so the page never says "charter only, no code yet"`);
-- assert.notStrictEqual(status.repoStage, 'charter-only', `${d}: status.json repoStage`);
-- }
-- const runs = rg.state === 'internal' || (st.code === true && st.deployed === true);
-- if (runs && st.stage !== 'closed' && rg.state !== 'live' && site.kind !== 'moved') { // a moved notice speaks for its address, not the product
-- assert.ok(site.launch, `${d}: the service runs, so sites.json says what its public launch waits for ("launch")`);
-- assert.ok(html.includes(`The public launch is waiting for ${site.launch.replace(/'/g, '&#39;')}.`), `${d}: the page says what the launch waits for`);
-- assert.match(html, /in development/, `${d}: labelled in development`);
-- assert.strictEqual(status.repoStage, 'service-running', `${d}: status.json repoStage`);
-- assert.strictEqual(status.launchWaitsFor, site.launch, `${d}: status.json launchWaitsFor`);
++ assert.match(html, /<meta name="robots" content="noindex(?:, follow)?">/, `${d}: noindex`);
++ for (const link of notice.links) assert.ok(html.includes(`href="${link.href}"`), `${d}: destination ${link.href}`);
++ for (const file of ['404.html', 'robots.txt', 'manifest.webmanifest', 'status.json', 'release.json']) {
++ assert.ok(exists(`dist/${d}/${file}`), `${d}: ${file}`);
}
-- if (rg.state === 'live' && rg.public_site === 'service' && !site.kind) {
-- assert.ok(html.includes(`href="${rg.origin}/"`), `${d}: links the live service ${rg.origin}`);
-- assert.strictEqual(status.repoStage, 'surface-of-a-live-service', `${d}: status.json repoStage`);
++ assert.match(read(`dist/${d}/404.html`), /<meta name="robots" content="noindex">/, `${d}: 404 is noindex`);
++ assert.ok(read(`dist/${d}/404.html`).includes(`href="https://${d}/"`), `${d}: 404 links home`);
++ assert.doesNotMatch(read(`dist/${d}/robots.txt`), /Sitemap:/, `${d}: no sitemap advertised`);
++ const release = JSON.parse(read(`dist/${d}/release.json`));
++ assert.strictEqual(release.service, notice.tld, `${d}: release service`);
++ assert.strictEqual(release.kind, 'notice', `${d}: release kind`);
++ assert.match(release.release, /^[0-9a-f]{12}$/, `${d}: release hash`);
++ if (notice.vhostOwner) {
++ assert.strictEqual(notice.vhostOwner, 'OpenVibers/OpenVibe.Bot');
++ assert.ok(!exists(`deploy/nginx/${d}.conf`), `${d}: owner provides the vhost`);
++ } else {
++ const conf = read(`deploy/nginx/${d}.conf`);
++ assert.ok(conf.includes(`server_name ${d}`), `${d}: server_name`);
++ assert.ok(conf.includes(`root /opt/openvibe.sites/dist/${d};`), `${d}: static root`);
++ assert.ok(conf.includes('error_page 404 /404.html;'), `${d}: true 404 page`);
++ assert.ok(conf.includes('try_files $uri $uri.html $uri/ =404;'), `${d}: unknown paths return 404`);
}
}
--// facts.json is current with the sibling checkouts when they are here (CI has none; skip there).
--const REPOS_DIR = process.env.OPENVIBERS_DIR || path.join(ROOT, '..');
--for (const [repo, f] of Object.entries(facts.repos)) {
-- const dir = path.join(REPOS_DIR, repo);
-- if (!fs.existsSync(path.join(dir, '.git')) || !f.status) continue;
-- let s;
-- try { s = JSON.parse(execFileSync('git', ['-C', dir, 'show', 'HEAD:STATUS.json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })); } catch { continue; }
-- assert.strictEqual(f.status.stage, s.stage || null, `${repo}: STATUS.json stage changed; run node scripts/facts.js and rebuild`);
-- assert.strictEqual(f.status.code, s.code === true, `${repo}: STATUS.json code changed; run node scripts/facts.js and rebuild`);
--}
--
--// A domain that a real service answers is never also a placeholder here (roadmap 7.2 rule 8).
--const LIVE_SERVICES = ['events.openvibe.network', 'billing.openvibe.network', 'openvibe.wiki', 'openvibe.blog', 'openvibe.codes', 'openvibe.community', 'openvibe.live', 'openvibe.media', 'openvibe.tools', 'openvibe.network'];
--for (const d of LIVE_SERVICES) {
-- assert.ok(!catalog.sites.some((s) => s.domain === d), `${d} is a running service and must not have a placeholder`);
--}
--const notices = catalog.sites.filter(s => s.kind).length;
--console.log(`sites build: ${pages} pages (${pages - notices} placeholders, ${notices} notices) current, honest and complete`);
++console.log(`sites build: ${notices.length} notices current; ${frozen.length} product domains frozen`);
ddiff --git a/test/run.js b/test/run.js
iindex f061312..6219c6e 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -1,7 +1,6 @@
#!/usr/bin/env node
/**
-- * Runs every test/*.test.js in its own process and fails if any fails. build.test.js rebuilds the pages in
-- * memory (`node build.js --check`) and checks them against sites.json and the committed facts.json;
++ * Runs every test/*.test.js in its own process and fails if any fails. build.test.js checks generated notices and the frozen product files;
* deploy-wrapper.test.js drives the deploy wrapper against a fake ovhost and a temp checkout. Neither
* needs the network or a running site.
*