openvibers/openvibe.shared Commits

Shared 4.1.1: released_at reads the commit time on release-layout services; showcase grids balance 5/6/9 cards (#67)

OP OpenVibers committed 12 Oct 2026 · e8929bd00d · 6 files, +55 −2

Two small fixes, found checking the sitemap `lastmod` sweep and an axe and layout pass over every home page.

- **`released_at` on release-layout services.**
  - The cause: under ovhost a release directory belongs to the deploying user (root), while the service runs as its own user. git therefore refuses the checkout ("fatal: detected dubious ownership", reproduced on openvibe-ovh in `/opt/openre.stream/current` as the service user).
  - The effect: `release.json`'s `released_at` fell back to the boot time on openvibe.live and openre.stream. Their static sitemap entries got no honest `lastmod`, and open tabs saw a "new" release at every restart.
  - The fix: `release.js`'s read-only git calls pass `-c safe.directory=<the release's real path>`. git honours that on the command line, and it is resolved through the `current` symlink, so only that directory is named.
- **Showcase grids** with five, six or nine cards use three columns at 900 px and wider, instead of four columns and a lonely last row. The Wiki and Events homes showed 4 + 2. The rule is CSS `:has()` in `showcase.js`, and `showcase.css` is regenerated.

Tests: the new `release-git.test.js` builds a git release reached through a `current` symlink and checks that `release` and `released_at` are the commit's, not the boot time. `showcase.test.js` checks that the stylesheet matches its module. The full suite passes 58 of 59 files; `floating-promises` is skipped without acorn, as usual. Version 4.1.1.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex ee96e3a..7fc506c 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,16 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 4.1.1 — 2026-10-12
++
++- **`release.js` reads the commit time on release-layout services.** A release directory belongs to the deploying
++ user while the service runs as its own, so git refused it ("detected dubious ownership") and `released_at` fell back
++ to the boot time — on openvibe.live and openre.stream, for example. The read-only git calls now pass
++ `-c safe.directory=<the release's real path>`, so `released_at` is the commit time again (the sitemaps' static
++ `lastmod` and open tabs' release checks use it).
++- **Showcase grids balance five, six or nine cards** in three columns on a wide screen (≥ 900 px), instead of four
++ and a lonely last row (the Wiki and Events homes showed 4 + 2).
++
## 4.1.0 — 2026-10-12
- **Every `shell.page()` site is an installable web app, with no per-site code.** A new `icons/` directory ships
ddiff --git a/package.json b/package.json
iindex 5669e88..2b7beb0 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.1.0",
++ "version": "4.1.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/release.js b/release.js
iindex c296f25..f1c0227 100644
---- a/release.js
++++ b/release.js
@@@ -51,8 +51,14 @@ const KINDS = ['style', 'content', 'script', 'server'];
const V1_0 = ['service', 'release', 'released_at', 'booted_at', 'contracts_version', 'packages', 'min_client_release', 'mixed_version_window_hours'];
const SCHEMA_PATH = 'openvibe-contracts/contracts/registry/release-manifest.v1.json';
++// A release directory belongs to the deploying user (root under ovhost) while the service runs as its own user, and
++// git then refuses the repository ("detected dubious ownership"): released_at fell back to the boot time on every
++// release-layout service. These reads are read-only, of this release's own checkout, so the directory is named safe
++// for them on the command line (git honours safe.directory there), resolved through the `current` symlink.
function git(root, args) {
-- try { return execFileSync('git', args, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); } catch { return ''; }
++ let real = root;
++ try { real = fs.realpathSync(root); } catch { /* keep root */ }
++ try { return execFileSync('git', ['-c', `safe.directory=${real}`, ...args], { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); } catch { return ''; }
}
function pkgVersion(name, root) {
ddiff --git a/showcase.css b/showcase.css
iindex 7d2b0ad..9450c4a 100644
---- a/showcase.css
++++ b/showcase.css
@@@ -18,6 +18,7 @@
.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}
.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}
.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
++@media (min-width:900px){.sc-grid:has(>:nth-child(5):last-child),.sc-grid:has(>:nth-child(6):last-child),.sc-grid:has(>:nth-child(9):last-child){grid-template-columns:repeat(3,minmax(0,1fr))}}
.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}
a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}
.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}
ddiff --git a/showcase.js b/showcase.js
iindex f3f40b8..bfdf88e 100644
---- a/showcase.js
++++ b/showcase.js
@@@ -184,6 +184,7 @@ const CSS = `
.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}
.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}
.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
++@media (min-width:900px){.sc-grid:has(>:nth-child(5):last-child),.sc-grid:has(>:nth-child(6):last-child),.sc-grid:has(>:nth-child(9):last-child){grid-template-columns:repeat(3,minmax(0,1fr))}}
.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}
a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}
.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}
ddiff --git a/test/release-git.test.js b/test/release-git.test.js
nnew file mode 100644
iindex 0000000..aabd84f
---- /dev/null
++++ b/test/release-git.test.js
@@@ -0,0 +1,35 @@
++'use strict';
++// release.js reads the release's commit and its time from git in the release's own checkout, reached the way ovhost's
++// release layout reaches it (a `current` symlink), with the directory named safe for those reads: a release owned by
++// the deploying user is otherwise refused by git ("dubious ownership") and released_at fell back to the boot time.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const { execFileSync } = require('child_process');
++const { createRelease } = require('../release');
++
++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-release-git-'));
++try {
++ const rel = path.join(tmp, 'releases', 'abc');
++ fs.mkdirSync(rel, { recursive: true });
++ const env = { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: '[email protected]', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: '[email protected]', GIT_AUTHOR_DATE: '2026-10-01T12:00:00Z', GIT_COMMITTER_DATE: '2026-10-01T12:00:00Z' };
++ execFileSync('git', ['init', '-q'], { cwd: rel, env });
++ fs.writeFileSync(path.join(rel, 'package.json'), '{"name":"x","version":"1.0.0"}\n');
++ execFileSync('git', ['add', '.'], { cwd: rel, env });
++ execFileSync('git', ['commit', '-q', '-m', 'release'], { cwd: rel, env });
++ const sha = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: rel }).toString().trim();
++ const current = path.join(tmp, 'current');
++ fs.symlinkSync(rel, current);
++
++ const m = createRelease({ service: 'probe', root: current, env: {}, schema: null }).manifest();
++ assert.strictEqual(m.release, sha, 'the commit, through the symlink');
++ assert.strictEqual(m.released_at, '2026-10-01T12:00:00.000Z', "the commit's time, not the boot time");
++ assert.notStrictEqual(m.released_at, m.booted_at);
++
++ const src = fs.readFileSync(path.join(__dirname, '..', 'release.js'), 'utf8');
++ assert.match(src, /'-c', `safe\.directory=\$\{real\}`/, 'git is told the release directory is safe to read');
++ console.log('release-git: ok');
++} finally {
++ fs.rmSync(tmp, { recursive: true, force: true });
++}