diff --git a/rf-diagnostics/app.js b/rf-diagnostics/app.js new file mode 100644 index 000000000..c546118ee --- /dev/null +++ b/rf-diagnostics/app.js @@ -0,0 +1,48 @@ +'use strict'; +const M=RFDiagnostics,$=id=>document.getElementById(id),ns='http://www.w3.org/2000/svg'; +let generation=0; +function render(rows){ + const last=rows.at(-1),kinds=[...new Set(rows.map(r=>M.modes[r.source_kind]))]; + $('mode').textContent=kinds.join(' + ')||'NO DATA'; + $('count').textContent=rows.length.toLocaleString(); + $('rms').textContent=last?.rms_dbfs==null?'—':last.rms_dbfs.toFixed(2); + $('rails').textContent=last?.rail_fraction==null?'—':(last.rail_fraction*100).toFixed(3)+'%'; + $('gaps').textContent=rows.length?rows.reduce((n,r)=>n+BigInt(r.missing_samples),0n).toLocaleString():'—'; + const reasons=new Map(); + for(const r of rows)for(const reason of new Set(r.invalid_reasons))reasons.set(reason,(reasons.get(reason)||0)+1); + $('reasons').replaceChildren(); + for(const [reason,count] of reasons){const li=document.createElement('li');li.textContent=reason+' · '+count.toLocaleString();$('reasons').append(li);} + $('meta').textContent=last?[ + 'Source: '+last.source_id,'Origin: '+last.source_kind+' (file/demo replay)', + 'Rate: '+(last.sample_rate_hz||'unknown')+' samples/s', + 'Centre: '+(last.center_frequency_hz??'unknown')+' Hz (unverified)', + 'Run / slot: '+last.run+' / '+last.slot_index, + 'Host monotonic ns: '+last.observed_monotonic_ns, + 'Absolute sample start: '+(last.sample_start??'unknown'), + 'Configuration verified: false','Valid for inference: false', + 'TTL: '+last.ttl_ms+' ms; replay freshness unknown' + ].join('\n'):'No observations'; + const tail=rows.slice(-500),values=tail.map(r=>r.rms_dbfs).filter(v=>v!==null); + const low=Math.min(...values),high=Math.max(...values),span=high-low||1; + $('chart').replaceChildren(); + let path=''; + tail.forEach((r,i)=>{ + if(r.rms_dbfs===null){path+=' ';return;} + const x=20+i*960/Math.max(1,tail.length-1),y=220-200*(r.rms_dbfs-low)/span; + path+=(i===0||tail[i-1].rms_dbfs===null?'M':'L')+x+','+y+' '; + if(r.missing_samples){const dot=document.createElementNS(ns,'circle');dot.setAttribute('cx',x);dot.setAttribute('cy',y);dot.setAttribute('r',4);dot.setAttribute('fill','#ffb66c');$('chart').append(dot);} + }); + const line=document.createElementNS(ns,'path');line.setAttribute('d',path);line.setAttribute('fill','none');line.setAttribute('stroke','#71e7db');line.setAttribute('stroke-width',2);$('chart').prepend(line); + $('range').textContent=values.length?'Range '+low.toFixed(2)+' to '+high.toFixed(2)+' dBFS · '+tail.length+' latest records':'No RMS observations'; + $('chart').setAttribute('aria-label',$('range').textContent); +} +$('demo').onclick=()=>{generation++;render(M.demo());$('status').textContent='Synthetic demonstration loaded. No hardware evidence.';}; +$('clear').onclick=()=>{generation++;render([]);$('file').value='';$('status').textContent='Cleared. No observations retained.';}; +$('file').onchange=async()=>{ + const ticket=++generation,file=$('file').files[0];if(!file)return; + try{ + if(file.size>M.MAX_BYTES)throw Error('File exceeds 10 MiB'); + const rows=M.parse(await file.text());if(ticket!==generation)return; + render(rows);$('status').textContent='Imported '+rows.length+' records. Source labels are unverified claims; this is file replay.'; + }catch(e){if(ticket!==generation)return;render([]);$('status').textContent='Import rejected: '+e.message;} +}; diff --git a/rf-diagnostics/index.html b/rf-diagnostics/index.html new file mode 100644 index 000000000..479103c69 --- /dev/null +++ b/rf-diagnostics/index.html @@ -0,0 +1,12 @@ + + + +RuView RF Diagnostics +

COGNITUM / RUVIEW / W1700K

NO DATA

RF diagnostics.

Understand the measurement before trusting the inference.

+

Local processing only. Maximum 10 MiB and 10,000 observations. No uploads.

+

Import bridge diagnostics or explore a clearly labelled synthetic example.

+ +

Latest RMS

—relative dBFS, not calibrated RF power

Latest rails

—samples at numeric limits

Reported missing

—sum of per record missing samples

Observations

0all v1 observations invalid
+

Relative RMS history

Record order, last 500 observations. Missing metrics break the line. Orange dots flag missing samples.

No observations

+

Invalidation reasons

  • No observations

Source metadata

No observations
+
diff --git a/rf-diagnostics/model.js b/rf-diagnostics/model.js new file mode 100644 index 000000000..f27e4fa29 --- /dev/null +++ b/rf-diagnostics/model.js @@ -0,0 +1,46 @@ +(function(root){ +'use strict'; +const MAX_BYTES=10*1024*1024, MAX_RECORDS=10000; +const fields='schema source_kind source_id observed_monotonic_ns run slot_index sample_start sample_rate_hz center_frequency_hz config_verified valid invalid_reasons sample_count rms_dbfs rail_fraction missing_samples event_count overrun_slices ttl_ms'.split(' '); +const modes={synthetic:'SYNTHETIC',live:'LIVE UNVERIFIED',captured_replay:'CAPTURED REPLAY'}; +function validate(r){ + const fail=(s)=>{throw Error(s)}; + if(!r||Array.isArray(r)||typeof r!=='object')fail('Expected an object'); + for(const k of Object.keys(r))if(!fields.includes(k))fail('Unsupported field: '+k); + for(const k of fields)if(!Object.hasOwn(r,k))fail('Missing field: '+k); + if(r.schema!=='ruview.rf_diagnostics.v1')fail('Unsupported schema'); + if(!Object.hasOwn(modes,r.source_kind)||r.source_id!=='w1700k')fail('Unsupported source'); + for(const k of ['observed_monotonic_ns','run','slot_index','sample_rate_hz','sample_count','missing_samples','event_count','overrun_slices','ttl_ms']) + if(!Number.isSafeInteger(r[k])||r[k]<0)fail('Invalid '+k); + if(r.ttl_ms!==1000)fail('Invalid rate or TTL'); + for(const k of ['sample_start','center_frequency_hz']) + if(r[k]!==null&&(!Number.isSafeInteger(r[k])||r[k]<0))fail('Invalid '+k); + if(r.rms_dbfs!==null&&(typeof r.rms_dbfs!=='number'||!Number.isFinite(r.rms_dbfs)))fail('Invalid RMS'); + if(r.rail_fraction!==null&&(typeof r.rail_fraction!=='number'||!Number.isFinite(r.rail_fraction)||r.rail_fraction<0||r.rail_fraction>1))fail('Invalid rail fraction'); + if(r.config_verified!==false||r.valid!==false)fail('v1 must remain unverified and invalid'); + if(!Array.isArray(r.invalid_reasons)||r.invalid_reasons.length>32||!r.invalid_reasons.includes('configuration_unverified')||r.invalid_reasons.some(x=>typeof x!=='string'||!/^[a-z_]{1,64}$/.test(x)))fail('Invalid reasons'); + if(![0,40000000,80000000,160000000,320000000].includes(r.sample_rate_hz))fail('Invalid sample rate'); + if(r.sample_start!==null)fail('Absolute sample start is unavailable in v1'); + if(r.center_frequency_hz!==null&&(r.center_frequency_hz<2000000000||r.center_frequency_hz>8000000000))fail('Invalid centre frequency'); + if(![0,2048].includes(r.sample_count)||r.missing_samples%2048!==0)fail('Invalid sample count'); + if(r.rms_dbfs!==null&&(r.rms_dbfs< -200||r.rms_dbfs>16||r.rail_fraction===null))fail('Invalid RMS or missing rail fraction'); + const reasons=['configuration_unverified','lost','event','overrun','stale','tag_mismatch','run_change']; + if(new Set(r.invalid_reasons).size!==r.invalid_reasons.length||r.invalid_reasons.length>7||r.invalid_reasons.some(x=>!reasons.includes(x)))fail('Unsupported invalid reason'); + return r; +} +function parse(text){ + if(typeof text!=='string'||new TextEncoder().encode(text).length>MAX_BYTES)throw Error('File exceeds 10 MiB'); + const lines=text.split(/\r?\n/).filter(x=>x.trim()); + if(!lines.length||lines.length>MAX_RECORDS)throw Error('Expected 1 to 10,000 records'); + return lines.map((s,i)=>{try{return validate(JSON.parse(s))}catch(e){throw Error('Record '+(i+1)+': '+e.message)}}); +} +function demo(){return Array.from({length:96},(_,i)=>({ + schema:'ruview.rf_diagnostics.v1',source_kind:'synthetic',source_id:'w1700k', + observed_monotonic_ns:i*100000000,run:1,slot_index:i,sample_start:null, + sample_rate_hz:40000000,center_frequency_hz:2437000000,config_verified:false,valid:false, + invalid_reasons:i%23===0?['configuration_unverified','lost']:['configuration_unverified'], + sample_count:2048,rms_dbfs:-34+Math.sin(i/8)*6,rail_fraction:i%23===0?0.03:0, + missing_samples:i%23===0?2048:0,event_count:0,overrun_slices:0,ttl_ms:1000}));} +const api={MAX_BYTES,MAX_RECORDS,modes,validate,parse,demo}; +if(typeof module==='object')module.exports=api;else root.RFDiagnostics=api; +})(globalThis); diff --git a/rf-diagnostics/style.css b/rf-diagnostics/style.css new file mode 100644 index 000000000..408dce670 --- /dev/null +++ b/rf-diagnostics/style.css @@ -0,0 +1 @@ +:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,sans-serif;background:#080f17;color:#d9e8ee}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 85% 0,#113642,transparent 50%)}main{max-width:1200px;margin:auto;padding:48px 28px}header{position:relative}.eyebrow{letter-spacing:.2em;color:#62d9da;font-size:12px;font-weight:700}h1{font-size:clamp(38px,7vw,76px);letter-spacing:-.055em;margin:18px 0}.intro{font-size:20px;color:#a4bcc6}.badge{display:inline-block;padding:8px 14px;border:1px solid #b89851;border-radius:32px;color:#f5d18c;font-size:12px;font-weight:bold}.controls{margin-top:32px;display:flex;gap:12px;flex-wrap:wrap;align-items:center}.controls p{flex-basis:100%;font-size:13px;color:#a4bcc6}button,.button{font:inherit;font-size:14px;border:1px solid #38616f;background:#152b37;color:#e7faff;border-radius:10px;padding:13px 18px;cursor:pointer}.button{background:#77e9dd;color:#082b2c;font-weight:700;position:relative;overflow:hidden}.button input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}button:hover,.button:hover{filter:brightness(1.2)}button:focus-visible,.button:focus-within{outline:3px solid #fff;outline-offset:3px}aside{border-left:3px solid #e5b85e;padding:16px 20px;background:#30291c;font-size:14px;line-height:1.6;color:#f5ddab}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:24px 0}.metrics article,.panel{border:1px solid #25414d;background:#0d1c26;border-radius:14px;padding:22px}.metrics p{font-size:13px;color:#9cb6c1}.metrics strong{font-size:30px;color:#79eddf;display:block;margin:16px 0}.metrics small{color:#9cb6c1;font-size:11px}.panel{margin-bottom:18px}h2{font-size:18px;margin-top:0}.panel p,footer{color:#9cb6c1;font-size:13px;line-height:1.6}svg{width:100%;height:auto;display:block;background:linear-gradient(#132c37,#0d1c26);border-radius:8px}.split{display:grid;grid-template-columns:1fr 1fr;gap:18px}pre{white-space:pre-wrap;word-break:break-word;color:#b7d1da;line-height:1.7;font-size:12px}li{margin-bottom:10px;overflow-wrap:anywhere}#status{font-size:14px;min-height:22px}footer{margin:24px 0} @media(max-width:700px){main{padding:28px 16px}.metrics{grid-template-columns:1fr 1fr}.split{grid-template-columns:1fr;gap:0}.metrics article{padding:14px}.metrics strong{font-size:24px}}