openvibers/openvibe.network Commits
Tools job proof (WS-L task 4): principal probe gets tools.job.create/read and events.event.read; /status and /api/v1/status show its last run (tools_job_proof) next to the developer path
Changes
@@@ -120,6 +120,11 @@ const DEFAULT_GRANTS = [['coupons', 'events.event.publish', 'openvibe.events', []],['coupons', 'sources.item.read', 'openvibe.sources', []],['host', 'events.event.publish', 'openvibe.events', []],++ // WS-L task 4: OpenVibe.Host's scheduled Tools job proof (openvibe-toolsjob.timer, OpenVibe.Examples++ // scripts/tools-job-proof.js) submits a converter job as `probe` and finds its tools.job.* events.++ ['probe', 'tools.job.create', 'openvibe.tools', []],++ ['probe', 'tools.job.read', 'openvibe.tools', []],++ ['probe', 'events.event.read', 'openvibe.events', []],// Wave 20: the Codes portal relays its release events.['codes', 'events.event.publish', 'openvibe.events', []],['ai', 'events.event.publish', 'openvibe.events', []], // ai.run.* (AI server/events.js, 2026-09-24)@@@ -27,6 +27,9 @@ const SLO_FILE = path.join(__dirname, '..', '..', 'docs', 'slo.json');// The daily developer path against production (OpenVibe.Host openvibe-devpath.timer, WS-N task 1): step// names and outcomes only, written by OpenVibe.Examples' developer-path --result.const DEVPATH_FILE = process.env.DEVPATH_RESULT_FILE || '/var/lib/openvibe-devpath/last.json';++// The Tools job proof (openvibe-toolsjob.timer, WS-L task 4): the same record from OpenVibe.Examples'++// tools-job-proof --result, read by the same loader.++const TOOLSJOB_FILE = process.env.TOOLSJOB_RESULT_FILE || '/var/lib/openvibe-devpath/tools-job.json';function loadDevPath(file = DEVPATH_FILE) {try {const d = JSON.parse(fs.readFileSync(file, 'utf8'));@@@ -117,7 +120,13 @@ function checksList(ready) {return items.length ? `<ul class="chk">${items.join('')}</ul>` : '';}--function renderPage(list, slo, generatedAt, devPath = null) {++/** A scheduled proof's step list (developer path, Tools job proof). */++function proofSteps(rec) {++ return `<ul class="st-sum">${rec.steps.map((st) => `<li class="st-${st.ok ? 'up' : st.skipped ? 'unknown' : 'down'}">${esc(st.name)}: ${st.ok ? 'ok' : st.skipped ? 'skipped' : 'failed'}</li>`).join('')}</ul>`;++}++const lastRun = (rec) => `Last run <time datetime="${esc(rec.finished_at)}">${esc(rec.finished_at)}</time>: <b>${rec.ok ? 'passed' : 'failed'}</b> (${rec.steps.filter((st) => st.ok).length}/${rec.steps.length} steps).`;++++function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null) {const counts = summary(list);const tr = list.map((r) => `<tr id="svc-${esc(r.id)}"><td><b>${esc(r.name)}</b><small>${esc(r.id)} · manifest: ${esc(r.manifest_status)}</small></td>@@@ -160,8 +169,12 @@ ${tr}</tbody></table><section aria-labelledby="h-devpath"><h2 id="h-devpath">Developer path</h2>--${devPath ? `<p class="lede">Once a day a test developer account goes from sign-in to a sandbox project, a Media upload, an Events publish and pull, a credential rotation and revocation, and cleanup, through the public API and the SDK. Last run <time datetime="${esc(devPath.finished_at)}">${esc(devPath.finished_at)}</time>: <b>${devPath.ok ? 'passed' : 'failed'}</b> (${devPath.steps.filter((st) => st.ok).length}/${devPath.steps.length} steps).</p>--<ul class="st-sum">${devPath.steps.map((st) => `<li class="st-${st.ok ? 'up' : st.skipped ? 'unknown' : 'down'}">${esc(st.name)}: ${st.ok ? 'ok' : st.skipped ? 'skipped' : 'failed'}</li>`).join('')}</ul>` : '<p class="lede">The daily developer path has not reported here yet.</p>'}++${devPath ? `<p class="lede">Once a day a test developer account goes from sign-in to a sandbox project, a Media upload, an Events publish and pull, a credential rotation and revocation, and cleanup, through the public API and the SDK. ${lastRun(devPath)}</p>++${proofSteps(devPath)}` : '<p class="lede">The daily developer path has not reported here yet.</p>'}++</section>++<section aria-labelledby="h-toolsjob"><h2 id="h-toolsjob">Tools job proof</h2>++${toolsJob ? `<p class="lede">Every six hours a service client submits a converter job to OpenVibe.Tools through the SDK, drops and reattaches its progress stream, downloads the result from OpenVibe.Media and finds the job's events in OpenVibe.Events. ${lastRun(toolsJob)}</p>++${proofSteps(toolsJob)}` : '<p class="lede">The Tools job proof has not reported here yet.</p>'}</section><section aria-labelledby="h-slo"><h2 id="h-slo">SLO categories (proposals)</h2><p class="lede">${esc(slo.note)} ${esc(slo.binding)} JSON: <a href="/api/v1/status/slo"><code>/api/v1/status/slo</code></a>.</p>@@@ -190,6 +203,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {exposure_states: exposure.STATES,exposure_summary: exposureSummary(list),developer_path: loadDevPath(),++ tools_job_proof: loadDevPath(TOOLSJOB_FILE),services: list,});});@@@ -199,7 +213,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {r.get('/status', (_req, res) => {const slo = { ...loadSlo(), pollSeconds: ecosystem.pollMs / 1000 };res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', 'no-cache, max-age=0').set('X-Robots-Tag', 'noindex, nofollow');-- res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath()));++ res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath(), loadDevPath(TOOLSJOB_FILE)));});return r;}@@@ -77,6 +77,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22assert.strictEqual(by.contracts.status, 'not-running');assert.strictEqual(by.contracts.label, `not running (library, released v${require('openvibe-contracts/package.json').version})`);for (const s of r.body.services) assert.ok(s.checked_at, `${s.id} says when it was checked`);++ assert.ok('developer_path' in r.body && 'tools_job_proof' in r.body, 'the scheduled proofs are in the API (null until they report)');assert.strictEqual(r.body.summary.up + r.body.summary.degraded + r.body.summary.down + r.body.summary['not-running'] + r.body.summary.unknown, contracts.services.manifests.length);// A required failure on the service is 'down', with its failed checks.@@@ -110,6 +111,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22assert.ok(/id="svc-tools"[\s\S]*?liveness only/.test(html));assert.ok(html.includes('<noscript>'), 'navigation without JavaScript');assert.ok(html.includes('SLO categories (proposals)'));++ assert.ok(html.includes('id="h-devpath"') && html.includes('id="h-toolsjob"'), 'both scheduled proofs have a section');const noScript = html.replace(/<script[\s\S]*?<\/script>/g, '');assert.ok(noScript.includes('aaaaaaaaaaaa'), 'the rows are in the HTML itself');
@@ -120,6 +120,11 @@ const DEFAULT_GRANTS = [
@@ -120,6 +120,11 @@ const DEFAULT_GRANTS = [
['coupons', 'events.event.publish', 'openvibe.events', []],
['coupons', 'events.event.publish', 'openvibe.events', []],
['coupons', 'sources.item.read', 'openvibe.sources', []],
['coupons', 'sources.item.read', 'openvibe.sources', []],
['host', 'events.event.publish', 'openvibe.events', []],
['host', 'events.event.publish', 'openvibe.events', []],
+ // WS-L task 4: OpenVibe.Host's scheduled Tools job proof (openvibe-toolsjob.timer, OpenVibe.Examples
+ // scripts/tools-job-proof.js) submits a converter job as `probe` and finds its tools.job.* events.
+ ['probe', 'tools.job.create', 'openvibe.tools', []],
+ ['probe', 'tools.job.read', 'openvibe.tools', []],
+ ['probe', 'events.event.read', 'openvibe.events', []],
// Wave 20: the Codes portal relays its release events.
// Wave 20: the Codes portal relays its release events.
['codes', 'events.event.publish', 'openvibe.events', []],
['codes', 'events.event.publish', 'openvibe.events', []],
['ai', 'events.event.publish', 'openvibe.events', []], // ai.run.* (AI server/events.js, 2026-09-24)
['ai', 'events.event.publish', 'openvibe.events', []], // ai.run.* (AI server/events.js, 2026-09-24)
@@ -27,6 +27,9 @@ const SLO_FILE = path.join(__dirname, '..', '..', 'docs', 'slo.json');
@@ -27,6 +27,9 @@ const SLO_FILE = path.join(__dirname, '..', '..', 'docs', 'slo.json');
// The daily developer path against production (OpenVibe.Host openvibe-devpath.timer, WS-N task 1): step
// The daily developer path against production (OpenVibe.Host openvibe-devpath.timer, WS-N task 1): step
// names and outcomes only, written by OpenVibe.Examples' developer-path --result.
// names and outcomes only, written by OpenVibe.Examples' developer-path --result.
const DEVPATH_FILE = process.env.DEVPATH_RESULT_FILE || '/var/lib/openvibe-devpath/last.json';
const DEVPATH_FILE = process.env.DEVPATH_RESULT_FILE || '/var/lib/openvibe-devpath/last.json';
+// The Tools job proof (openvibe-toolsjob.timer, WS-L task 4): the same record from OpenVibe.Examples'
+// tools-job-proof --result, read by the same loader.
+const TOOLSJOB_FILE = process.env.TOOLSJOB_RESULT_FILE || '/var/lib/openvibe-devpath/tools-job.json';
function loadDevPath(file = DEVPATH_FILE) {
function loadDevPath(file = DEVPATH_FILE) {
try {
try {
const d = JSON.parse(fs.readFileSync(file, 'utf8'));
const d = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -117,7 +120,13 @@ function checksList(ready) {
@@ -117,7 +120,13 @@ function checksList(ready) {
return items.length ? `<ul class="chk">${items.join('')}</ul>` : '';
return items.length ? `<ul class="chk">${items.join('')}</ul>` : '';
}
}
-function renderPage(list, slo, generatedAt, devPath = null) {
+/** A scheduled proof's step list (developer path, Tools job proof). */
+function proofSteps(rec) {
+ return `<ul class="st-sum">${rec.steps.map((st) => `<li class="st-${st.ok ? 'up' : st.skipped ? 'unknown' : 'down'}">${esc(st.name)}: ${st.ok ? 'ok' : st.skipped ? 'skipped' : 'failed'}</li>`).join('')}</ul>`;
+}
+const lastRun = (rec) => `Last run <time datetime="${esc(rec.finished_at)}">${esc(rec.finished_at)}</time>: <b>${rec.ok ? 'passed' : 'failed'}</b> (${rec.steps.filter((st) => st.ok).length}/${rec.steps.length} steps).`;
+
+function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null) {
const counts = summary(list);
const counts = summary(list);
const tr = list.map((r) => `<tr id="svc-${esc(r.id)}">
const tr = list.map((r) => `<tr id="svc-${esc(r.id)}">
<td><b>${esc(r.name)}</b><small>${esc(r.id)} · manifest: ${esc(r.manifest_status)}</small></td>
<td><b>${esc(r.name)}</b><small>${esc(r.id)} · manifest: ${esc(r.manifest_status)}</small></td>
@@ -160,8 +169,12 @@ ${tr}
@@ -160,8 +169,12 @@ ${tr}
</tbody>
</tbody>
</table>
</table>
<section aria-labelledby="h-devpath"><h2 id="h-devpath">Developer path</h2>
<section aria-labelledby="h-devpath"><h2 id="h-devpath">Developer path</h2>
-${devPath ? `<p class="lede">Once a day a test developer account goes from sign-in to a sandbox project, a Media upload, an Events publish and pull, a credential rotation and revocation, and cleanup, through the public API and the SDK. Last run <time datetime="${esc(devPath.finished_at)}">${esc(devPath.finished_at)}</time>: <b>${devPath.ok ? 'passed' : 'failed'}</b> (${devPath.steps.filter((st) => st.ok).length}/${devPath.steps.length} steps).</p>
-<ul class="st-sum">${devPath.steps.map((st) => `<li class="st-${st.ok ? 'up' : st.skipped ? 'unknown' : 'down'}">${esc(st.name)}: ${st.ok ? 'ok' : st.skipped ? 'skipped' : 'failed'}</li>`).join('')}</ul>` : '<p class="lede">The daily developer path has not reported here yet.</p>'}
+${devPath ? `<p class="lede">Once a day a test developer account goes from sign-in to a sandbox project, a Media upload, an Events publish and pull, a credential rotation and revocation, and cleanup, through the public API and the SDK. ${lastRun(devPath)}</p>
+${proofSteps(devPath)}` : '<p class="lede">The daily developer path has not reported here yet.</p>'}
+</section>
+<section aria-labelledby="h-toolsjob"><h2 id="h-toolsjob">Tools job proof</h2>
+${toolsJob ? `<p class="lede">Every six hours a service client submits a converter job to OpenVibe.Tools through the SDK, drops and reattaches its progress stream, downloads the result from OpenVibe.Media and finds the job's events in OpenVibe.Events. ${lastRun(toolsJob)}</p>
+${proofSteps(toolsJob)}` : '<p class="lede">The Tools job proof has not reported here yet.</p>'}
</section>
</section>
<section aria-labelledby="h-slo"><h2 id="h-slo">SLO categories (proposals)</h2>
<section aria-labelledby="h-slo"><h2 id="h-slo">SLO categories (proposals)</h2>
<p class="lede">${esc(slo.note)} ${esc(slo.binding)} JSON: <a href="/api/v1/status/slo"><code>/api/v1/status/slo</code></a>.</p>
<p class="lede">${esc(slo.note)} ${esc(slo.binding)} JSON: <a href="/api/v1/status/slo"><code>/api/v1/status/slo</code></a>.</p>
@@ -190,6 +203,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {
@@ -190,6 +203,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {
exposure_states: exposure.STATES,
exposure_states: exposure.STATES,
exposure_summary: exposureSummary(list),
exposure_summary: exposureSummary(list),
developer_path: loadDevPath(),
developer_path: loadDevPath(),
+ tools_job_proof: loadDevPath(TOOLSJOB_FILE),
services: list,
services: list,
});
});
});
});
@@ -199,7 +213,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {
@@ -199,7 +213,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {
r.get('/status', (_req, res) => {
r.get('/status', (_req, res) => {
const slo = { ...loadSlo(), pollSeconds: ecosystem.pollMs / 1000 };
const slo = { ...loadSlo(), pollSeconds: ecosystem.pollMs / 1000 };
res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', 'no-cache, max-age=0').set('X-Robots-Tag', 'noindex, nofollow');
res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', 'no-cache, max-age=0').set('X-Robots-Tag', 'noindex, nofollow');
- res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath()));
+ res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath(), loadDevPath(TOOLSJOB_FILE)));
});
});
return r;
return r;
}
}
@@ -77,6 +77,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
@@ -77,6 +77,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
assert.strictEqual(by.contracts.status, 'not-running');
assert.strictEqual(by.contracts.status, 'not-running');
assert.strictEqual(by.contracts.label, `not running (library, released v${require('openvibe-contracts/package.json').version})`);
assert.strictEqual(by.contracts.label, `not running (library, released v${require('openvibe-contracts/package.json').version})`);
for (const s of r.body.services) assert.ok(s.checked_at, `${s.id} says when it was checked`);
for (const s of r.body.services) assert.ok(s.checked_at, `${s.id} says when it was checked`);
+ assert.ok('developer_path' in r.body && 'tools_job_proof' in r.body, 'the scheduled proofs are in the API (null until they report)');
assert.strictEqual(r.body.summary.up + r.body.summary.degraded + r.body.summary.down + r.body.summary['not-running'] + r.body.summary.unknown, contracts.services.manifests.length);
assert.strictEqual(r.body.summary.up + r.body.summary.degraded + r.body.summary.down + r.body.summary['not-running'] + r.body.summary.unknown, contracts.services.manifests.length);
// A required failure on the service is 'down', with its failed checks.
// A required failure on the service is 'down', with its failed checks.
@@ -110,6 +111,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
@@ -110,6 +111,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
assert.ok(/id="svc-tools"[\s\S]*?liveness only/.test(html));
assert.ok(/id="svc-tools"[\s\S]*?liveness only/.test(html));
assert.ok(html.includes('<noscript>'), 'navigation without JavaScript');
assert.ok(html.includes('<noscript>'), 'navigation without JavaScript');
assert.ok(html.includes('SLO categories (proposals)'));
assert.ok(html.includes('SLO categories (proposals)'));
+ assert.ok(html.includes('id="h-devpath"') && html.includes('id="h-toolsjob"'), 'both scheduled proofs have a section');
const noScript = html.replace(/<script[\s\S]*?<\/script>/g, '');
const noScript = html.replace(/<script[\s\S]*?<\/script>/g, '');
assert.ok(noScript.includes('aaaaaaaaaaaa'), 'the rows are in the HTML itself');
assert.ok(noScript.includes('aaaaaaaaaaaa'), 'the rows are in the HTML itself');