openvibers/openvibe.games Commits

Games: /updates — what shipped on openvibe.games (the shared update log) (#78)

OP OpenVibers committed 29 days ago · 627ff50962 · 5 files, +141 −0

openvibe.games gets `/updates`: what shipped on the site, from the network's shared update log (`openvibe-shared/frame` `updatesBody` + `shipped.js`), inside the OpenVibe Frame like the other OpenVibe sites. A site-wide crawl found openvibe.help linking it (404).

- `apps/server/src/net/updatesPage.ts` renders the page once per process with `openvibe-shared/shell`, the same way `legalPages.ts` renders the legal pages. It has one `<h1>`, a canonical URL, a public 5-minute cache and 405 for other methods. It is served on the portal host only.
- The sitemap, llms.txt and llms-full.txt list it (`discovery.ts` `publicPageDocs`).

`updatesPage.test.ts` checks the page, other paths, POST and HEAD. The discovery, httpServer and legalPages tests pass (45), and typecheck and eslint are clean.

Changes

ddiff --git a/apps/server/src/net/discovery.ts b/apps/server/src/net/discovery.ts
iindex ef8402a..b0e1daa 100644
---- a/apps/server/src/net/discovery.ts
++++ b/apps/server/src/net/discovery.ts
@@@ -174,6 +174,13 @@ function publicPageDocs(): PublicPageDoc[] {
text: pack.tagline,
priority: 0.8,
})),
++ {
++ title: 'What shipped',
++ url: `${SITE}/updates`,
++ note: 'every change deployed to OpenVibe.Games, newest first',
++ text: "The update log of OpenVibe.Games: every change deployed to the site and its games, newest first, each a commit from the OpenVibers repositories linked to the change itself (the network's shared log; the whole network's is at https://openvibe.network/updates).",
++ priority: 0.4,
++ },
]
}
ddiff --git a/apps/server/src/net/httpServer.ts b/apps/server/src/net/httpServer.ts
iindex 09df457..c4e6308 100644
---- a/apps/server/src/net/httpServer.ts
++++ b/apps/server/src/net/httpServer.ts
@@@ -31,6 +31,7 @@ import { resolveClientAddress } from './clientAddress.js'
import { sendNotFound, type NotFoundLink } from './notFound.js'
import { discoveryHandler, homeJsonLdTags } from './discovery.js'
import { legalHandler } from './legalPages.js'
++import { updatesHandler } from './updatesPage.js'
import { indexnowHandler, type SharedIndexNow } from './indexnow.js'
/** The portal page with its JSON-LD, built once per file version: the game server's event loop never reads the file
@@@ -200,6 +201,8 @@ export function createHttpServer(
]
// The site's own /terms, /privacy and /dmca (openvibe-shared/legal), built once for the process.
const serveLegal = legalHandler()
++ // What shipped on the site (the shared update log), as on every OpenVibe site: the portal's host only.
++ const serveUpdates = updatesHandler()
// GET /<key>.txt — the IndexNow key file, before the static files so it is never shadowed by a
// build artifact of the same name. Absent when no key is configured.
const serveIndexNow = indexnow ? indexnowHandler(indexnow) : null
@@@ -243,6 +246,7 @@ export function createHttpServer(
// The site's own legal pages (plan T11 decision 6): /terms, /privacy, /dmca. Before the SPA and the
// static fallback so they never resolve to a file or the landing page.
if (serveLegal(req, res)) return
++ if (!playHost && serveUpdates(req, res)) return
const requestedMapPlace = new URL(req.url ?? '/', 'http://games.local').searchParams.get('game')
if (url === '/map.json' && requestedMapPlace && requestedMapPlace !== defaultPlaceId) {
res.writeHead(404, { 'content-type': 'application/json', 'cache-control': 'no-store' })
ddiff --git a/apps/server/src/net/updatesPage.test.ts b/apps/server/src/net/updatesPage.test.ts
nnew file mode 100644
iindex 0000000..461df5e
---- /dev/null
++++ b/apps/server/src/net/updatesPage.test.ts
@@@ -0,0 +1,46 @@
++import { describe, expect, it } from 'vitest'
++import type { IncomingMessage, ServerResponse } from 'node:http'
++import { updatesHandler } from './updatesPage.js'
++
++function call(handler: ReturnType<typeof updatesHandler>, url: string, method = 'GET') {
++ let status = 0
++ let headers: Record<string, string> = {}
++ let body = ''
++ const res = {
++ writeHead: (code: number, h?: Record<string, string>) => {
++ status = code
++ headers = h ?? {}
++ },
++ end: (b?: string) => {
++ body = b ?? ''
++ },
++ } as unknown as ServerResponse
++ const handled = handler({ url, method } as IncomingMessage, res)
++ return { handled, status, headers, body }
++}
++
++describe('GET /updates: the shared update log', () => {
++ const handler = updatesHandler()
++
++ it('answers the log in the Frame, with one heading and its canonical address', () => {
++ const r = call(handler, '/updates')
++ expect(r.handled).toBe(true)
++ expect(r.status).toBe(200)
++ expect(r.headers['content-type']).toMatch(/^text\/html/)
++ expect(r.headers['cache-control']).toBe('public, max-age=300')
++ expect(r.body).toContain('<h1>What shipped on OpenVibe.Games</h1>')
++ expect(r.body).toContain('data-ov-shipped="log" data-service="games"')
++ expect((r.body.match(/<h1[\s>]/g) ?? []).length).toBe(1)
++ expect(r.body).toContain('<link rel="canonical" href="https://openvibe.games/updates">')
++ expect(r.body).toMatch(/<script src="\/shared\/shipped\.js\?v=[0-9a-f]+" defer><\/script>/)
++ })
++
++ it('leaves every other path alone and refuses other methods', () => {
++ expect(call(handler, '/updates/x').handled).toBe(false)
++ expect(call(handler, '/').handled).toBe(false)
++ const post = call(handler, '/updates', 'POST')
++ expect([post.handled, post.status]).toEqual([true, 405])
++ const head = call(handler, '/updates?x=1', 'HEAD')
++ expect([head.status, head.body]).toEqual([200, ''])
++ })
++})
ddiff --git a/apps/server/src/net/updatesPage.ts b/apps/server/src/net/updatesPage.ts
nnew file mode 100644
iindex 0000000..60ff0d8
---- /dev/null
++++ b/apps/server/src/net/updatesPage.ts
@@@ -0,0 +1,81 @@
++import type { IncomingMessage, ServerResponse } from 'node:http'
++import { createRequire } from 'node:module'
++
++/**
++ * GET /updates: what shipped on openvibe.games, the network's shared update log (openvibe-shared/frame updatesBody and
++ * shipped.js) inside the OpenVibe Frame, as every OpenVibe site serves it. openvibe.help links each site's /updates.
++ *
++ * The page is rendered once per process by openvibe-shared/shell (this server is plain node:http, like legalPages.ts);
++ * the log itself is filled in the browser from openvibe.network's changelog.
++ */
++interface Shell {
++ page(o: Record<string, unknown>): string
++}
++interface Frame {
++ updatesBody(o: { service: string; siteName: string }): string
++}
++interface Serve {
++ url(p: string): string
++}
++
++const SITE_NAME = 'OpenVibe.Games'
++const SITE_URL = 'https://openvibe.games'
++const NETWORK_URL = 'https://openvibe.network'
++
++export function updatesHandler(req: NodeRequire = createRequire(import.meta.url)) {
++ const shell = req('openvibe-shared/shell') as Shell
++ const frame = req('openvibe-shared/frame') as Frame
++ const serve = req('openvibe-shared/serve') as Serve
++ let html: string | null = null
++ const render = (): string => {
++ if (html) return html
++ const links = [
++ { label: 'Play', href: '/play' },
++ { label: 'Leaderboard', href: '/leaderboard.html' },
++ ]
++ html = shell.page({
++ name: SITE_NAME,
++ service: 'games',
++ lang: 'en',
++ title: `What shipped on ${SITE_NAME}`,
++ siteName: SITE_NAME,
++ description: `Every change deployed to ${SITE_NAME}, newest first.`,
++ canonical: `${SITE_URL}/updates`,
++ robots: 'index, follow',
++ navbar: {
++ service: 'games',
++ apiBase: NETWORK_URL,
++ links,
++ history: { type: 'page', title: SITE_NAME },
++ },
++ footer: {
++ service: 'games',
++ variant: 'full',
++ mount: '#ov-footer',
++ brandName: SITE_NAME,
++ updates: '/updates',
++ },
++ home: '/',
++ navLinks: links,
++ body: `<div id="navbar-mount"></div>\n<main id="main" class="page">\n${frame.updatesBody({ service: 'games', siteName: SITE_NAME })}\n</main>\n<script src="${serve.url('shipped.js')}" defer></script>`,
++ })
++ return html
++ }
++ return (rq: IncomingMessage, res: ServerResponse): boolean => {
++ const url = (rq.url ?? '/').split('?')[0] ?? '/'
++ if (url !== '/updates') return false
++ if (rq.method !== 'GET' && rq.method !== 'HEAD') {
++ res.writeHead(405, { allow: 'GET, HEAD' })
++ res.end()
++ return true
++ }
++ const body = render()
++ res.writeHead(200, {
++ 'content-type': 'text/html; charset=utf-8',
++ 'cache-control': 'public, max-age=300',
++ 'content-length': String(Buffer.byteLength(body)),
++ })
++ res.end(rq.method === 'HEAD' ? undefined : body)
++ return true
++ }
++}
ddiff --git a/test/apps__server__src__net__updatesPage.test.js b/test/apps__server__src__net__updatesPage.test.js
nnew file mode 100644
iindex 0000000..a08e7f2
---- /dev/null
++++ b/test/apps__server__src__net__updatesPage.test.js
@@@ -0,0 +1,3 @@
++// file: 'apps/server/src/net/updatesPage.test.ts'
++// covers: 'apps/server/src/net'
++// Generated by node test/run.js --index; do not edit.