Repositories openvibers openvibe.report official

openvibers/openvibe.report

Open source analytics for OpenVibe: every number honest, yours, your channel's, your app's and the network's, with The Rundown and after-show reports.

Get it

Get this repository

Clone over HTTPS

A Git token is made for this copy and shown once. It can read and write the repositories you own, and expires in an hour.

git clone https://git.openvibe.codes/openvibers/openvibe.report.git
Clone to my computer

OpenVibe Link asks you on your computer. The folder is inside the root you chose in Link, and the request ends after two minutes.

Clone to my computer
Open in Codes

A Codes session clones from our disk: no network, no GitHub quota. When this repository has a GitHub twin, Codes pushes there too.

Open in Codes
Files at main
.env.exampleReport: views and engaged viewers per item, creator and site (#200)16 h ago3.1 KB
.githubopenvibe-shared v4.223.0 (was v4.214.0) (#199)19 h ago
.gitignoreThe node_modules symlink is not source (deploy refused with EISDIR) (#173)3 days ago102 B
AGENTS.mdAGENTS.md: the context an agent starts from (#198)1 days ago11.0 KB
CHANGELOG.mdAI: carry OpenVibe.Bench's pages as Report's AI section (#132)5 days ago3.0 KB
CITATION.cffREADME first screen and CITATION.cff (#169)3 days ago529 B
CLAUDE.mdAGENTS.md: the context an agent starts from (#198)1 days ago11 B
LICENSEOpenVibe.Report v1: every number on OpenVibe, honest. The network's open numbers, The Rundown (what streamers talked about) and each stream's after-show report, with an open JSON API; stateless over Network's open stats and Live's Rundown and recaps16 days ago33.7 KB
README.mdopenvibe-shared v4.235.0 (was v4.234.0) (#218)10 min ago21.3 KB
SECURITY.mdReport as the analytics authority, expand and shadow (REP-2) (#4)15 days ago1.5 KB
STATUS.jsonopenvibe-shared v4.235.0 (was v4.234.0) (#218)10 min ago5.0 KB
deployAllow rating API preflights through Report nginx (#178)3 days ago
docsRead shared Bench ratings across Report AI boards (#177)3 days ago
migrationsAI performance: histograms, task cost and time, /api/v1/ai/performance (#212)4 h ago
package-lock.jsonopenvibe-shared v4.235.0 (was v4.234.0) (#218)10 min ago60.9 KB
package.jsonopenvibe-shared v4.235.0 (was v4.234.0) (#218)10 min ago1.1 KB
publicThe AI model comparison moves to OpenVibe.Compare: /ai pages answer 301 (#215)3 h ago
scriptsSession reports: the owner chooses who sees it (#110)6 days ago
serverThe AI model comparison moves to OpenVibe.Compare: /ai pages answer 301 (#215)3 h ago
testThe AI model comparison moves to OpenVibe.Compare: /ai pages answer 301 (#215)3 h ago

README.md

View file

OpenVibe.Report

Every number on OpenVibe, honest.

Live site: https://openvibe.report · API docs: https://openvibe.services · For AI: https://openvibe.report/llms.txt
Run it: npm ci && npm run dev (Node 22 or newer) · Tests: npm test

Status: v0.1.0 service, now with its own analytics database and a shadow of Network's numbers (REP-2, plan
2026-10-28 §4.8 and §5.4). The pages still read where they read today; REPORT_SOURCE=report switches the numbers
when the cutover is approved.
Domain: openvibe.report · Port: 5080 · Service id: report · Env prefix: OV_ / REPORT_
License: AGPL-3.0 (same as every OpenVibe service).

The Report room of the OpenVibe network: the open numbers every site publishes, The Rundown of what streamers talked
about, and each stream's after-show report.

Purpose

OpenVibe.Report is the analytics authority in shadow:

  • It ingests the analytics OpenVibe.Network records — live.stream.ended for creator analytics and the shared
    navbar's anonymous frame beacon — and Network's open-stats answer (GET /api/v1/stats?days=7|30|90) into its own
    PostgreSQL tables (migrations/).
  • It computes the same numbers Network answers, site-wide, per site and per channel, by UTC day.
  • It quotes money from OpenVibe.Billing through Billing's read API; it never derives a price, fee or amount.
  • It shadows the AI Exchange behind REPORT_AI_EXCHANGE (off): Billing's billing.credit.settled@1
    settlements are summed by UTC day, resolved exact model id and provider access. The raw settlement is not stored,
    and Report never sets a rate, a price or a balance.
  • It shadows: a job compares the same window from Network and from Report's tables and records every difference
    in report_shadow_diffs.

The pages and GET /api/v1/stats answer from REPORT_SOURCE, which defaults to network: today's behaviour. Setting
REPORT_SOURCE=report makes them answer from Report's tables. The Rundown and after-show reports still come from
OpenVibe.Live in both modes.

| Source | Read | Used for |
|---|---|---|
| OpenVibe.Network | GET /api/v1/stats?days=7\|30\|90 | /, /stats, GET /api/v1/stats while REPORT_SOURCE=network; also the ingest source |
| Report's tables | report_metric_days, report_metric_windows, report_streams | the same routes while REPORT_SOURCE=report |
| OpenVibe.Live — The Rundown | GET /api/rundown/, /api/rundown/topics, /api/rundown/t/:slug, /api/rundown/e/:streamId, /api/rundown/s/:username | /rundown, /rundown/t/:slug, /rundown/e/:streamId, /rundown/s/:username |
| OpenVibe.Live — after-show reports | GET /api/recap/:streamId, GET /api/recap/channel/:username | /after-show/:streamId, /c/:username |
| OpenVibe.Bench | GET /api/v1/kinds, /api/v1/boards/:kind?type=elo\|pareto\|cost, /api/v1/benchmarks, /api/v1/rankings, /api/v1/runs, /api/v1/overview, /api/v1/models/:model, /api/v1/models/:model/summary | /ai, /ai/:kind, /ai/<benchmark>, /ai/models/:model, /ai/compare, /ai/method, /ai/feed.xml, GET /api/v1/ai/results |
| OpenVibe.AI | GET /api/v1/exchange/catalog | GET /api/v1/ai/leaderboard, /api/v1/ai/models/:id, /api/v1/ai/providers, /api/v1/ai/compare?models=, /api/v1/ai/picks |
| OpenVibe.Billing | GET /api/v1/stats, GET /api/v1/transactions?subject=… | the revenue read for a channel; network money counts |
| OpenVibe.Billing — settlements, through Events | billing.credit.settled@1 on POST /internal/events (only with REPORT_AI_EXCHANGE=1) | the AI Exchange aggregates and the staff page/API |

A source that does not answer is null, after one short log line; the page (or API route) becomes a 404. The service
never invents a zero and never throws a source error at the reader.

What Report owns now

  • The analytics tables in migrations/0001_initial.sql: report_inbox (event ids already handled),
    report_events, report_streams, report_frame_days, report_metric_days, report_metric_windows,
    report_metric_json, report_dimensions, report_shadow_runs and report_shadow_diffs.
  • The AI meter in migrations/0002_ai_usage.sql and 0003_ai_usage_surface.sql: report_ai_usage (raw calls,
    kept 90 days) and report_ai_usage_days; the consumer is server/analytics/ai-usage.js.
  • The AI Exchange aggregates in migrations/0005_ai_exchange.sql: report_ai_exchange_days (day, model,
    provider access, requests, sandbox requests, the four token counts, exact nano-USD retail/shortfall/supply/margin),
    report_ai_exchange_seen (settlement ids already counted) and report_ai_exchange_rejected (invalid payloads).
    The consumer is server/analytics/ai-exchange.js.
  • The computation in server/analytics/: ingest.js (idempotent writes), query.js (Network's stats shape and
    a channel's creator analytics), shadow.js (the comparison), ai-exchange.js (the Exchange aggregates).
  • The jobs in server/jobs/analytics.js: ingest every five minutes (7, 30 and 90-day windows) and shadow every
    fifteen minutes (7 days by default).

Does not own

  • Money — OpenVibe.Billing. Report reads it with billing.stats.read and billing.balance.read; it holds no
    ledger and computes no price.
  • The Rundown and the after-show reports — OpenVibe.Live. Report quotes them, including their counts and text.
  • The benchmark results — OpenVibe.Bench. Report renders its rows and never derives a score, cost or time; Bench stayed the authority when its public pages moved here (2026-11-08) and its old HTML pages answer 301 to /ai counterparts.
  • Identity, sessions and writes — Report has no sign-in, no cookies and no public write route. POST /internal/events is loopback-only and HMAC-verified.
  • The Shared Frame — openvibe-shared (navbar, footer, theme-loader, static files), served from this site's own
    pinned copy at /shared/*.

Pages

All server-rendered in server/views/index.js; every dynamic value goes through esc().

| Page | What it shows |
|---|---|
| / | the 7-day open numbers, the Rundown's counts and lead, and the trending topics |
| /stats?days=7\|30\|90 | the network totals and a first look at every site (anything else is 30) |
| /me?days=7\|30\|90 | signed in only, private, noindex: your sites and activity (Network's history), what you made, your AI and Vibes, with a switch per section to publish it (the owner, 2026-11-09) |
| /u/:username | a person's published stats: only the sections they turned on, with Person JSON-LD; noindex and no numbers until they publish |
| /rundown | on air, the lead, the latest streams and every topic; canonical is OpenVibe.Live's /rundown |
| /rundown/t/:slug | one topic, its takes and related topics; canonical is Live's /rundown/t/:slug |
| /rundown/e/:streamId | one stream as an episode, with its segments; canonical is Live's /rundown/e/:streamId |
| /rundown/s/:username | one streamer's episodes and topics; canonical is Live's /rundown/:username |
| /after-show/:streamId | one after-show report |
| /c/:username | one channel's latest after-show reports |
| /ai | the AI overview: every published benchmark with its leader, runs and last run; every task kind with its leader, quality, cost and speed; a link to the method and compare pages |
| /ai/:kind | one task kind: Elo rank, quality against cost, cost per solved, the verified runs on its benchmarks, and the latest runs |
| /ai/<benchmark> | one public Bench benchmark: verified rankings with score, cost, speed and last run, a score-against-cost SVG, and the latest runs |
| /ai/models/:model | a model's runs over time, its best and worst tasks, cost, per-benchmark record and recent runs |
| /ai/compare | two or three models on the same benchmarks, side by side |
| /ai/method | how a score is made, how runs are judged, verified vs submitted, and what the counts mean |
| /ai/feed.xml | new public runs as RSS; the same rows are GET /api/v1/ai/results |
| /ai/exchange | staff only, with REPORT_AI_EXCHANGE=1: Billing's settlements by day, model and access, a daily chart and the exact amounts. Needs a signed-in person whose staff role holds staff.site.view |

A bad path parameter (slug, stream id, username) or a source that answered nothing renders views.notFound() with
status 404. Pages are Cache-Control: public, max-age=120.
AI pages use a 60-second Bench read cache and serve its last good copy with the read time during an outage. Their
HTML is no-store so the outage notice stays current.

API

All under /api/v1, CORS *, Cache-Control: public, max-age=120 (the popularity pair: 300), 120 requests a minute per address:

GET /api/v1/stats?days=7|30|90        REPORT_SOURCE=network|report
GET /api/v1/popular?limit=5           the front door's ranked sites and tools (public, aggregate)
GET /api/v1/popular/co-usage          opt-in site pairs for "Try next", small groups omitted
GET /api/v1/rundown
GET /api/v1/rundown/topics
GET /api/v1/rundown/t/:slug
GET /api/v1/after-show/:streamId
GET /api/v1/c/:username
GET /api/v1/ai/results            the new AI runs, newest first
GET /api/v1/me/stats?days=7|30|90  the signed-in person's own numbers (private, no-store)
PUT /api/v1/me/stats/published    the five per-section switches (same-origin, JSON)
GET /api/v1/u/:name/stats         only the sections that person published (404 when none)

A bad parameter or a missing source answer is a 404 application/problem+json body built with
openvibe-contracts http.sendProblem. The API is read-only except for the person's own publish switches; other
methods get 405. GET /api/v1/me/stats and PUT /api/v1/me/stats/published answer the report.me-stats@1 document
(the switch request is report.me-stats-publish@1); the public route copies only the published sections from the
person's last snapshot, so a section a source did not answer for is absent, never a zero.

/api/v1/popular ranks Report's own daily and window numbers: opens today (UTC, still counting) and over seven days,
the seven before that for growth, and the distinct signed-in people Network's cross-site history counts. A tool is a
destination beside a site. The answer is public and aggregate only, held five minutes. /api/v1/popular/co-usage
reads only people who switched their sites section public (report_me_stats.published); it returns pair counts, never
a person, username or subject, and a pair below three people is left out.

The AI Exchange JSON read is the staff exception (with REPORT_AI_EXCHANGE=1): a Network service token with the
same report.ai_usage.read capability the AI meter uses, never cached, no person or project in the answer. The
HTML page is gated by the signed-in person's staff role (staff.site.view) instead, like the other staff pages.

GET /api/v1/ai/exchange?from=YYYY-MM-DD&to=YYYY-MM-DD   the Exchange aggregates, staff only

The AI performance rows (report.ai-performance@1, plan L40/L41) answer the speed, reliability, tokens and money of
the calls Report meters. Model, provider and operation are public and show only a group with at least 20 calls; system,
task, place and harness need a signed-in person with staff.site.view or a service token with
report.ai_usage.read, and are never cached. The percentiles come from log-scale histograms and are the upper bound
of the bucket a call lands in; a task's cost and time are the sums of the calls that share its task_id.

GET /api/v1/ai/performance?group=model|provider|operation&days=7|30|90      public, 20-call floor
GET /api/v1/ai/performance?group=system|task|place|harness&days=7|30|90    staff, filters allowed
Internal (loopback only)
POST /internal/events                       OpenVibe.Events delivery (v2 HMAC, REPORT_EVENTS_SECRET)
GET  /internal/shadow?days=7&limit=5        the latest shadow runs and their differences
POST /internal/shadow?days=7                run a comparison now
GET  /internal/metrics?scope=&id=&days=     the flat report.metric@1 rows
GET  /internal/channels/:channel/analytics  a channel's creator analytics plus Billing revenue

nginx refuses /internal/ from outside; the app also checks the socket is loopback. The flat rows are proposed as
report.metric@1 in [docs/report.metric.v1.json](docs/report.metric.v1.json) for Contracts to own.

Ingest and idempotency

  • live.stream.ended → one report_streams row (same fields as Network's creator_streams) and the channel/day
    counts. A redelivered event id is a no-op; a corrected copy with a new event id replaces the stream and does not
    count it twice.
  • network.frame.hit / network.frame.day → report_frame_days (day, host, hits).
  • GET /api/v1/stats → report_metric_days, report_metric_windows, report_metric_json and site dimensions,
    keyed by the answer's content hash. Polling the same numbers again is a no-op.
  • Every delivery is claimed in report_inbox (openvibe-sdk/events createPgInbox) in the same transaction as its
    writes.
  • billing.credit.settled (with REPORT_AI_EXCHANGE=1) → report_ai_exchange_days, keyed by day, model and
    provider access, and report_ai_exchange_seen by settlement id. The payload is validated against
    billing.credit.settled@1; an invalid one is counted in report_ai_exchange_rejected and never aggregated.
    Replaying the same settlement under a new event id counts once. A sandbox settlement only increments
    sandbox_requests. The raw settlement is never stored. With the flag off, the delivery is ignored before anything
    is written.
  • Billing's other money still comes from Billing's read API, never from an event.

Configuration

Every value comes from the environment (production: /etc/openvibe/report.env). See [.env.example](.env.example).

| Name | Default | Meaning |
|---|---|---|
| NODE_ENV | development | production switches the base URL and is required for a real deploy |
| PORT | 5080 | the loopback port nginx proxies to |
| HOST | 127.0.0.1 | listen address |
| BASE_URL | https://openvibe.report in production, otherwise http://localhost:5080 | canonical links, sitemap and llms.txt |
| TRUST_PROXY | 2 | Express trust proxy (the app sets no cookies, but the per-address limit uses the client address) |
| DATABASE_URL | unset in development | Report's PostgreSQL through PgBouncer; without it development uses PGlite |
| DATABASE_DIRECT_URL | unset | the owner connection migrations run on |
| REPORT_PGLITE_DIR | ./data/pglite | the embedded development database |
| REPORT_SOURCE | network | network keeps today's behaviour; report answers from Report's tables |
| OV_NETWORK_INTERNAL_URL | http://127.0.0.1:4000 | Network's loopback origin (its manifest's internalOrigin) |
| OV_NETWORK_URL | https://openvibe.network | the OAuth token endpoint's origin |
| OV_LIVE_INTERNAL_URL | http://127.0.0.1:3000 | Live's loopback origin |
| OV_BENCH_INTERNAL_URL | https://bench.openvibe.services | Bench's public read origin, optionally an internal origin |
| OV_AI_INTERNAL_URL | https://api.openvibe.exchange | AI's public Exchange catalogue and measured provider offers (/api/v1/exchange/catalog; the ai.openvibe.services vhost does not pass it) |
| OV_BILLING_INTERNAL_URL | http://127.0.0.1:4600 | Billing's loopback origin |
| OV_OAUTH_CLIENT_ID / OV_OAUTH_CLIENT_SECRET | report / unset | Report's client-credentials token for openvibe.billing |
| REPORT_BILLING_TOKEN | unset | an explicit Billing bearer token for an operator run |
| REPORT_EVENTS_SECRET | unset | the HMAC secret for POST /internal/events (32+ chars, comma list for rotation) |
| REPORT_AI_EXCHANGE | 0 | 1 consumes Billing's billing.credit.settled events into the AI Exchange aggregates and opens /ai/exchange and GET /api/v1/ai/exchange; off consumes nothing |
| REPORT_JOBS | 1 | 0 turns the ingest and shadow jobs off |
| REPORT_JOBS_ON_BOOT | 1 | run both jobs once shortly after boot |
| REPORT_INGEST_INTERVAL_MS | 300000 | how often the ingest job runs |
| REPORT_SHADOW_INTERVAL_MS | 900000 | how often the shadow job runs |
| REPORT_SHADOW_WINDOW_DAYS | 7 | the shadow window (7, 30 or 90) |
| REPORT_API_RATE_LIMIT_PER_MIN | 120 | per-address API requests a minute |
| REPORT_READY_TIMEOUT_MS | 2500 | how long a readiness probe may take |
| RELEASE_COMMIT | unset | /release.json's release; otherwise read from git in the checkout |

Run and test

npm install
npm start            # http://127.0.0.1:5080; development uses the embedded PGlite database
npm test             # every test/*.test.js, in its own process

Most tests stub both sources and pass store: null, so they need no database. The analytics tests use
test/helpers/db.js (createTestDb from openvibe-sdk/testing) with a migrated PGlite, or the PostgreSQL containers
with REPORT_TEST_STORE=pg. They cover ingest idempotency, every metric against a fixture's expected numbers, the
shadow job's difference and clean run, GET /internal/shadow, the source flag, the Billing revenue read with a stub,
the AI Exchange aggregation/validation/replay/BigInt/sandbox/staff-gate/flag-off cases, and the existing
pages/API/health/crawl behaviour.

node --check is run over server/, test/ and public/ by CI (.github/workflows/ci.yml, the shared
OpenVibe.Shared test workflow).

Cutover

  1. Run the ingest and shadow jobs long enough to cover a full 90-day window, and check
    GET /internal/shadow has zero differences (or only explained ones).
  2. Create Report's first-party grants on Network: billing.stats.read and billing.balance.read for the report
    service (and the Network read grants when Report starts pulling creator analytics directly).
  3. Set REPORT_SOURCE=report in /etc/openvibe/report.env and restart; keep the Network ingest running.
  4. Leave Network writing during the dual-write window. Live's ANALYTICS_SOURCE can then move to Report; Network's
    /stats becomes a 301 to openvibe.report/stats.
  5. After the N-1 window, a Contracts migration removes Network's analytics tables. Rollback before that is
    REPORT_SOURCE=network and a restart.

Deploy

The reference files are in deploy/:

  • deploy/systemd/openvibe-report.service — User=ubuntu, WorkingDirectory=/opt/openvibe.report,
    EnvironmentFile=/etc/openvibe/report.env, PORT=5080, SIGTERM with a 10 s drain.
  • deploy/nginx/openvibe.report.conf — HTTP → HTTPS, www → apex 301, Let's Encrypt certificates under
    /etc/letsencrypt/live/openvibe.report/, proxy to 127.0.0.1:5080, the same security headers as Help's vhost,
    /api/ GET-only with a coarse rate limit, and /internal/ refused from outside.

/api/ready reports network and live as optional checks: one or both down is status: "degraded" with HTTP 200.
When DATABASE_URL is configured, the database check is included and is required only after REPORT_SOURCE=report.

Security

Every source text is escaped before it reaches HTML. Path parameters are checked against fixed patterns before any
request. Report's own sign-in serves /you and the staff page, which reads the signed-in person's staff role. Its
database has public channel facts, the AI meter and aggregate AI Exchange numbers; a raw settlement (a person, a
project, money) is never stored. /internal/ is loopback-only, and POST /internal/events verifies the Events v2
HMAC. See [SECURITY.md](SECURITY.md).

<!-- versions:start -->

  • openvibe-contracts: v0.421.0
  • openvibe-sdk: v0.161.0
  • openvibe-shared: v4.235.0
  • openvibe-publishing: v1.7.0

<!-- versions:end -->

AI market data and provenance

The AI pages and GET /api/v1/ai/leaderboard and /api/v1/ai/providers read
https://api.openvibe.exchange/api/v1/exchange/market alongside the routed catalog.
Report follows every market cursor and caches the complete result for 120 seconds.
An upstream failure retains the last complete market snapshot with stale: true;
without a snapshot, Report falls back to the routed catalog.

The market is public (exchange.market@1, cached 60 seconds). It accepts limit
(1–500, default 100), opaque cursor, maker, open_weights=true|false, and
kind=chat|embedding|stt|image. Responses contain offers, total_models,
total_offers, next_cursor, generated_at, and the models.dev metadata snapshot's
source, checked_at, and stale state. Each offer identifies model, provider,
provider_model, routed, provider_access, and the preset's uncensored flag.
Prices are USD per 1M tokens (in, out, cached), with price_source pointing
to that provider's own page and price_checked_at. Unknown prices remain null.
A latest observed price can differ from the price currently approved for routing.

Model fields include maker, context, max output, open weights, licence, release
date, knowledge cutoff and modalities. field_sources names the provider page or
https://models.dev/api.json for each known field. Metadata refreshes daily on
read; a failed refresh preserves the previous copy and marks it stale.

Comparison rows include every market model, rated rows first, then unrated rows.
Their first provider offer has the lowest known own-page output price, breaking
ties by input price; input/output/cached prices always come from that same offer.
Provider measurements join only routed offers. Other offers display “Not routed
here”; missing measurements on routed offers display “Not measured yet”.