lesavka: host no-upload receiver observer at dedicated HTTPS path
Some checks failed
Tests / Declarative: Post Actions failed: 42, skipped: 89, passed: 4273

This commit is contained in:
jenkins 2026-10-02 17:02:43 -05:00
parent d114ce71e8
commit e035cadb4d
12 changed files with 348 additions and 0 deletions

View File

@ -0,0 +1,28 @@
// A cumulative, one-credit metadata reporter: never copies or sends audio.
class Activity extends AudioWorkletProcessor {
constructor() {
super(); this.frames=0; this.inputFrames=0; this.zeroFrames=0;
this.energy=0; this.peak=0; this.credit=true; this.next=0; this.stopped=false;
this.port.onmessage=({data})=>{ if(data==='ack')this.credit=true; if(data==='stop')this.stopped=true; };
}
process(inputs,outputs) {
if(this.stopped || this.frames >= sampleRate*125) return false;
const block=outputs[0]?.[0]?.length ?? 128;
const samples=inputs[0]?.[0];
this.frames+=block;
if(samples) for(const value of samples) {
this.inputFrames++; this.energy+=value*value;
this.peak=Math.max(this.peak,Math.abs(value));
if(value===0)this.zeroFrames++;
}
// Output is left at zero: no monitoring/playback or acoustic feedback.
if(this.credit && this.frames>=this.next) {
this.port.postMessage({inputFrames:this.inputFrames,processedFrames:this.frames,
exactZeroFrames:this.zeroFrames,rms:this.inputFrames ? Math.sqrt(this.energy/this.inputFrames) : 0,
peak:this.peak,sampleRate});
this.credit=false; this.next=this.frames+sampleRate;
}
return true;
}
}
registerProcessor('av-activity',Activity);

View File

@ -0,0 +1,53 @@
# Static assets only; no API, uploads, credentials or outbound connections.
apiVersion: apps/v1
kind: Deployment
metadata:
name: lesavka-av-observer
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels: {app: lesavka-av-observer}
template:
metadata:
labels: {app: lesavka-av-observer}
spec:
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile: {type: RuntimeDefault}
containers:
- name: static
image: nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
command: [/usr/sbin/nginx]
args: [-c, /etc/nginx/observer/nginx.conf, -g, "daemon off;"]
ports: [{name: http, containerPort: 8080}]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 100m, memory: 64Mi}
readinessProbe:
httpGet: {path: /lesavka-av-test/, port: http}
livenessProbe:
httpGet: {path: /lesavka-av-test/, port: http}
initialDelaySeconds: 10
volumeMounts:
- {name: config, mountPath: /etc/nginx/observer, readOnly: true}
- {name: assets, mountPath: /usr/share/nginx/html/lesavka-av-test, readOnly: true}
- {name: tmp, mountPath: /tmp}
volumes:
- name: config
configMap: {name: lesavka-av-observer-nginx}
- name: assets
configMap: {name: lesavka-av-observer-assets}
- name: tmp
emptyDir: {medium: Memory, sizeLimit: 8Mi}

View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'none'; media-src 'self' blob:; style-src 'self'; object-src 'none'; base-uri 'none'">
<title>Lesavka — bundled A/V observer</title>
<link rel="stylesheet" href="style.css">
<main><h1>Bundled A/V observer</h1>
<p>No recording, uploads or saved camera/microphone content. Select the RCT's Lesavka video and microphone devices.</p>
<div><button id="devices">Choose devices…</button>
<label>Video <select id="video-device"></select></label>
<label>Microphone <select id="audio-device"></select></label></div>
<div><button id="start" disabled>Start 2-minute check</button><button id="stop" disabled>Stop</button><button id="export" disabled>Save metrics</button></div>
<p id="status" role="status">Stopped</p>
<video id="preview" autoplay muted playsinline></video>
<pre id="metrics">No observations.</pre>
<p>Sync and capture-to-RCT freshness: <strong>not measured by this observer</strong>.
Video callback gaps can include browser scheduling. Silence is not proof of audio loss.
Use the qualified paired-marker/clock-bounded probe for release acceptance.</p>
</main><script type="module" src="observer.js"></script></html>

View File

@ -0,0 +1,23 @@
# Reuse the existing domain/certificate, without changing its homepage route.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lesavka-av-observer
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts: [bstein.dev]
secretName: bstein-dev-home-tls
rules:
- host: bstein.dev
http:
paths:
- path: /lesavka-av-test
pathType: Prefix
backend:
service:
name: lesavka-av-observer
port: {number: 80}

View File

@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
- network-policy.yaml
configMapGenerator:
- name: lesavka-av-observer-nginx
files: [nginx.conf]
- name: lesavka-av-observer-assets
files:
- index.html
- observer.js
- metrics.js
- activity-worklet.js
- style.css

View File

@ -0,0 +1,30 @@
// Metadata only. No frames or PCM are retained. These are OBSERVER metrics,
// never an automatically passing sync/freshness/artifact report.
export class VideoMetrics {
constructor() { this.last = null; this.intervals = []; this.callbacks = 0; this.unobserved = 0; this.resets = 0; }
observe(timeMs, presentedFrames) {
if (!Number.isFinite(timeMs) || !Number.isSafeInteger(presentedFrames) || presentedFrames < 0) return;
if (this.last) {
if (timeMs <= this.last.timeMs || presentedFrames <= this.last.presentedFrames) this.resets++;
else {
if (this.intervals.length < 15000) this.intervals.push(timeMs-this.last.timeMs);
this.unobserved += Math.max(0,presentedFrames-this.last.presentedFrames-1);
}
}
this.last = {timeMs,presentedFrames}; this.callbacks++;
}
summary() {
const sorted = [...this.intervals].sort((a,b)=>a-b);
return {callbacks:this.callbacks,unobservedPresentedFrames:this.unobserved,clockOrCounterResets:this.resets,
callbackIntervalP95Ms:sorted.length ? sorted[Math.ceil(sorted.length*.95)-1] : null,
callbackGapMaxMs:sorted.at(-1) ?? null,
note:"Browser presentation callbacks; gaps are not proof of transport loss or corrupted video."};
}
}
export function receipt(video,audio,settings,elapsedMs,reason) {
return {schema:"lesavka.rct-av-observer.v1",kind:"diagnostic-only",reason,elapsedMs,
settings,video:video.summary(),audio,
syncMeasured:false,freshnessMeasured:false,artifactsMeasured:false,
releaseEligible:false,recordedMedia:false};
}

View File

@ -0,0 +1,12 @@
# The observer only serves static files; it has no reason to initiate traffic.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: lesavka-av-observer
spec:
podSelector:
matchLabels: {app: lesavka-av-observer}
policyTypes: [Ingress, Egress]
ingress:
- ports: [{protocol: TCP, port: 8080}]
egress: []

View File

@ -0,0 +1,35 @@
worker_processes 1;
pid /tmp/nginx.pid;
error_log /dev/stderr warn;
events { worker_connections 128; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
server_tokens off;
client_max_body_size 1k;
client_body_temp_path /tmp/client;
proxy_temp_path /tmp/proxy;
fastcgi_temp_path /tmp/fastcgi;
uwsgi_temp_path /tmp/uwsgi;
scgi_temp_path /tmp/scgi;
server {
listen 8080;
absolute_redirect off;
root /usr/share/nginx/html;
if ($request_method !~ ^(GET|HEAD)$) { return 405; }
add_header Cache-Control "no-store" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(), display-capture=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; media-src 'self' blob:; connect-src 'none'; worker-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
location = /lesavka-av-test { return 308 /lesavka-av-test/; }
location = /lesavka-av-test/ { try_files /lesavka-av-test/index.html =404; }
location ~ ^/lesavka-av-test/(index\.html|observer\.js|metrics\.js|activity-worklet\.js|style\.css)$ {
try_files $uri =404;
}
location / { return 404; }
}
}

View File

@ -0,0 +1,117 @@
import {VideoMetrics,receipt} from './metrics.js';
const el=id=>document.getElementById(id);
let generation=0, current=null, last=null;
const stopTracks=stream=>stream?.getTracks().forEach(track=>track.stop());
function release(run) {
if(run.released)return;
run.released=true;
clearTimeout(run.timer); clearTimeout(run.setupTimer);
if(run.callback)el('preview').cancelVideoFrameCallback(run.callback);
run.node?.port.postMessage('stop'); run.node?.disconnect(); run.source?.disconnect();
stopTracks(run.stream);
// Do not wait on a browser AudioContext close before fencing a stopped run.
run.context?.close().catch(()=>{});
}
function stop(reason='operator') {
generation++;
const run=current; current=null;
if(run) {
release(run);
last=receipt(run.video,run.audio,run.settings,performance.now()-run.started,reason);
el('metrics').textContent=JSON.stringify(last,null,2);
el('export').disabled=false;
}
el('preview').srcObject=null;
el('start').disabled=!el('audio-device').value || !el('video-device').value;
el('stop').disabled=true; el('devices').disabled=false;
el('status').textContent=`Stopped · ${reason}`;
}
el('devices').onclick=async()=>{
stop('selecting devices');
const ticket=++generation;
el('devices').disabled=true; el('start').disabled=true; el('stop').disabled=false;
el('status').textContent='Requesting device permission…';
const timer=setTimeout(()=>{if(generation===ticket)stop('permission timeout');},20000);
try {
const stream=await navigator.mediaDevices.getUserMedia({audio:true,video:true});
stopTracks(stream);
if(generation!==ticket)return;
const devices=await navigator.mediaDevices.enumerateDevices();
if(generation!==ticket)return;
for(const [id,kind] of [['video-device','videoinput'],['audio-device','audioinput']]) {
const select=el(id), saved=select.value;
select.replaceChildren(new Option('Select Lesavka device',''));
for(const d of devices.filter(d=>d.kind===kind)) select.add(new Option(d.label||'Unnamed device',d.deviceId));
select.value=saved;
}
el('status').textContent='Select both Lesavka devices, then Start.';
el('devices').disabled=false; el('stop').disabled=true;
} catch { if(generation===ticket)stop('device permission unavailable'); }
finally {clearTimeout(timer);}
};
for(const id of ['audio-device','video-device']) el(id).onchange=()=>{
if(current)stop('device selection changed');
el('start').disabled=!el('audio-device').value||!el('video-device').value;
};
el('start').onclick=async()=>{
if(current || !el('audio-device').value || !el('video-device').value)return;
if(!el('preview').requestVideoFrameCallback || !window.AudioWorkletNode) {
el('status').textContent='This browser lacks the required observation APIs.';return;
}
const ticket=++generation;
const run={started:performance.now(),video:new VideoMetrics(),audio:null,settings:{}};
current=run; last=null;
el('start').disabled=true; el('devices').disabled=true; el('stop').disabled=false; el('export').disabled=true;
el('status').textContent='Starting…';
run.setupTimer=setTimeout(()=>{if(current===run)stop('setup timeout');},20000);
try {
const stream=await navigator.mediaDevices.getUserMedia({
video:{deviceId:{exact:el('video-device').value}},
audio:{deviceId:{exact:el('audio-device').value},echoCancellation:false,noiseSuppression:false,autoGainControl:false},
});
if(generation!==ticket){stopTracks(stream);return;}
run.stream=stream;
for(const track of stream.getTracks())track.onended=()=>{if(current===run)stop('device ended');};
const vs=stream.getVideoTracks()[0].getSettings(),as=stream.getAudioTracks()[0].getSettings();
run.settings={video:{width:vs.width,height:vs.height,frameRate:vs.frameRate},
audio:{sampleRate:as.sampleRate,channelCount:as.channelCount,echoCancellation:as.echoCancellation,
noiseSuppression:as.noiseSuppression,autoGainControl:as.autoGainControl}};
run.context=new AudioContext();
await run.context.audioWorklet.addModule('activity-worklet.js');
if(generation!==ticket)return;
run.node=new AudioWorkletNode(run.context,'av-activity');
run.node.onprocessorerror=()=>{if(current===run)stop('audio observer error');};
run.node.port.onmessage=({data})=>{
if(current!==run)return;
run.audio=data; run.node.port.postMessage('ack');
el('metrics').textContent=JSON.stringify(receipt(run.video,run.audio,run.settings,performance.now()-run.started,'observing'),null,2);
};
run.source=run.context.createMediaStreamSource(new MediaStream(stream.getAudioTracks()));
run.source.connect(run.node);run.node.connect(run.context.destination);
await run.context.resume();
if(generation!==ticket)return;
el('preview').srcObject=stream;
await el('preview').play();
if(generation!==ticket)return;
clearTimeout(run.setupTimer);
run.started=performance.now();
function frame(now,metadata) {
if(current!==run)return;
run.video.observe(metadata.expectedDisplayTime,metadata.presentedFrames);
run.callback=el('preview').requestVideoFrameCallback(frame);
}
run.callback=el('preview').requestVideoFrameCallback(frame);
run.timer=setTimeout(()=>{if(current===run)stop('120 seconds complete');},120000);
el('status').textContent='Observing both devices · no recording';
} catch { if(current===run)stop('capture/setup failed'); }
finally { if(current!==run)release(run); }
};
el('stop').onclick=()=>stop();
el('export').onclick=()=>{
if(!last)return;
const url=URL.createObjectURL(new Blob([JSON.stringify(last,null,2)],{type:'application/json'}));
const link=document.createElement('a');link.href=url;link.download='lesavka-av-observer.json';link.click();
setTimeout(()=>URL.revokeObjectURL(url),1000);
};
window.addEventListener('pagehide',()=>stop('page closed'));
document.addEventListener('visibilitychange',()=>{if(document.hidden&&current)stop('page hidden; timing invalid');});

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: Service
metadata:
name: lesavka-av-observer
spec:
selector: {app: lesavka-av-observer}
ports: [{name: http, port: 80, targetPort: http}]

View File

@ -0,0 +1,7 @@
:root { color-scheme: dark; font: 15px system-ui,sans-serif; }
body { margin: 24px; background: #171b22; color: #e7edf6; }
main { max-width: 1000px; margin: auto; }
button,select { padding: 8px; margin: 4px; max-width: 100%; }
label { display: inline-block; }
video { width: 100%; max-height: 55vh; background: #000; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }

View File

@ -18,6 +18,7 @@ resources:
- vaultwarden-cred-sync-cronjob.yaml
- validation-jobs/portal-onboarding-e2e-test-job.yaml
- ingress.yaml
- av-observer
images:
- name: registry.bstein.dev/bstein/bstein-dev-home-frontend
newTag: 0.1.1-541 # {"$imagepolicy": "bstein-dev-home:bstein-dev-home-frontend:tag"}