phase 1: unfirehose reconstruction from session JSONL ingest

Source: ~/.unfirehose/unfirehose.db (project_id=81, 4 sessions covering
2026-03-29 through 2026-04-05). Reconstructed via chronological replay
of Write/Edit tool_input on file_paths under /home/fox/zebra-report/.

stats:
  files reconstructed:    20
  writes baselined:       all (zero missing)
  edits applied:          68
  edits unapplied:        8 (1 SKIP pre-baseline, 6 FAIL old_string drift, 1 AMBIGUOUS)

unapplied edits represent small drift in 6 files; baseline content for
each is intact. quality verification deferred to phase 2.

recovered tree:
  CLAUDE.md, Makefile
  src/{tx,rx,pulse,carrier,chat,bt}.c
  include/{modem,zebra}.h
  test/{functional,integration,unit}.c, test/test.h
  web/{index,kernel}.html, web/blog/style.css
  blog/build.py, blog/posts/{001-volume-modem,002-sse-chatroom}.md

report: /tmp/zebra_recover_report.txt
script: /tmp/zebra_recover.py
This commit is contained in:
Russell Ballestrini 2026-05-27 13:51:14 -04:00
commit b77da42bbe
20 changed files with 4062 additions and 0 deletions

483
web/kernel.html Normal file
View file

@ -0,0 +1,483 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>zebra kernel</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: monospace;
background: #fff;
color: #000;
padding: 2rem;
max-width: 640px;
}
h1 {
font-family: serif;
font-size: 3rem;
font-weight: normal;
letter-spacing: 0.02em;
line-height: 1;
margin-bottom: 0.2rem;
}
.sub {
font-size: 0.75rem;
color: #555;
margin-bottom: 2.5rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
section { margin-bottom: 2rem; }
h2 {
font-family: serif;
font-size: 1.1rem;
font-weight: normal;
border-bottom: 1px solid #000;
padding-bottom: 0.25rem;
margin-bottom: 0.8rem;
}
button {
background: #fff;
color: #000;
border: 1px solid #000;
padding: 0.4rem 1.2rem;
font-family: monospace;
font-size: 0.85rem;
cursor: pointer;
}
button:hover:not(:disabled) { background: #f0f0f0; }
button:disabled { opacity: 0.3; cursor: default; }
button.invert { background: #000; color: #fff; }
button.invert:hover:not(:disabled) { background: #333; }
.row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.dot {
width: 9px; height: 9px; border-radius: 50%;
border: 1px solid #000; background: #fff;
flex-shrink: 0; transition: background 0.3s;
}
.dot.on { background: #000; }
label, .note { font-size: 0.75rem; color: #555; }
.note { margin-top: 0.4rem; line-height: 1.5; }
pre {
border: 1px solid #ccc;
padding: 0.6rem 0.9rem;
font-size: 0.78rem;
overflow-x: auto;
line-height: 1.5;
margin-bottom: 0.5rem;
background: #fafafa;
}
textarea {
width: 100%;
font-family: monospace;
font-size: 0.78rem;
border: 1px solid #ccc;
padding: 0.5rem;
resize: vertical;
background: #fafafa;
color: #000;
line-height: 1.5;
}
textarea:focus { outline: 1px solid #000; border-color: #000; }
textarea[readonly] { background: #f5f5f5; color: #333; }
input[type="text"] {
font-family: monospace;
font-size: 0.78rem;
border: 1px solid #ccc;
padding: 0.4rem 0.6rem;
width: 100%;
background: #fafafa;
color: #000;
margin-bottom: 0.5rem;
}
input:focus { outline: 1px solid #000; border-color: #000; }
.field-row { display: flex; gap: 0.5rem; align-items: flex-start; margin-bottom: 0.5rem; }
.field-row textarea { flex: 1; }
.field-row button { flex-shrink: 0; white-space: nowrap; }
.key-display {
font-family: monospace;
font-size: 0.7rem;
word-break: break-all;
background: #fafafa;
border: 1px solid #ccc;
padding: 0.5rem;
color: #333;
margin-bottom: 0.5rem;
cursor: pointer;
user-select: all;
}
.key-display:hover { border-color: #000; }
.status-line {
font-size: 0.75rem;
color: #555;
margin-top: 0.3rem;
min-height: 1.1em;
}
.status-line.ok { color: #000; font-weight: bold; }
.status-line.err { color: #c00; }
video { display: none; }
hr { border: none; border-top: 1px solid #e0e0e0; margin: 2rem 0; }
</style>
</head>
<body>
<video id="v" autoplay loop muted></video>
<h1>zebra kernel</h1>
<p class="sub">volume modem &nbsp;&middot;&nbsp; stereo carrier &nbsp;&middot;&nbsp; e2e encrypted</p>
<!-- ============================================================ -->
<!-- CARRIER -->
<!-- ============================================================ -->
<section>
<h2>carrier</h2>
<div class="row">
<div class="dot" id="dot"></div>
<button id="btn-audio">start audio</button>
<label id="audio-status">offline</label>
</div>
<p class="note">
Starts a stereo audio stream — registers this tab as a named sink input
in PulseAudio (pavucontrol, pactl) with channels=2. The C clients
modulate L and R volume independently to transmit data.
</p>
</section>
<!-- ============================================================ -->
<!-- KEYS -->
<!-- ============================================================ -->
<section>
<h2>keys</h2>
<p class="note" style="margin-bottom:0.75rem">
ECDH P-256 key pair. Generated once, stored in localStorage.
Share your public key with the other side. Paste theirs below.
Both sides derive the same AES-256-GCM key &mdash; no key ever leaves the browser.
</p>
<p style="font-size:0.75rem;margin-bottom:0.3rem;color:#555">your public key (click to copy)</p>
<div class="key-display" id="pub-display" title="click to copy">generating...</div>
<div class="row" style="margin-bottom:0.75rem">
<button id="btn-regen">regenerate keys</button>
<span class="status-line" id="key-status"></span>
</div>
<p style="font-size:0.75rem;margin-bottom:0.3rem;color:#555">peer public key (paste here)</p>
<input type="text" id="peer-key-input" placeholder="paste peer's public key...">
<div class="row">
<button id="btn-peer" class="invert">derive shared key</button>
<span class="status-line" id="peer-status"></span>
</div>
</section>
<!-- ============================================================ -->
<!-- ENCRYPT / DECRYPT -->
<!-- ============================================================ -->
<section>
<h2>encrypt</h2>
<div class="field-row">
<textarea id="plaintext-in" rows="3" placeholder="plaintext message..."></textarea>
<button id="btn-encrypt">encrypt &rarr;</button>
</div>
<textarea id="ciphertext-out" rows="3" readonly placeholder="ciphertext (base64) appears here..."></textarea>
<p class="note">Copy ciphertext &rarr; pipe through <code>./tx</code> or <code>./bt</code></p>
</section>
<section>
<h2>decrypt</h2>
<div class="field-row">
<textarea id="ciphertext-in" rows="3" placeholder="paste received ciphertext (base64)..."></textarea>
<button id="btn-decrypt">decrypt &rarr;</button>
</div>
<textarea id="plaintext-out" rows="3" readonly placeholder="decrypted plaintext appears here..."></textarea>
<div class="status-line" id="dec-status"></div>
</section>
<hr>
<!-- ============================================================ -->
<!-- SETUP -->
<!-- ============================================================ -->
<section>
<h2>find this tab</h2>
<pre>./tx -l
pactl list sink-inputs short</pre>
</section>
<section>
<h2>battle toads (stereo, 2x bandwidth)</h2>
<pre><code># auto-negotiate baud, dual-channel stereo
./rx -s RX_SINK -t TX_SINK # terminal 1: benchmark + send offer
./tx -s TX_SINK -r RX_SINK # terminal 2: wait for offer + READY
# or fixed baud
./bt -T MY_SINK -R THEIR_SINK -b 500</code></pre>
<p class="note">
L and R channels carry independent UART streams simultaneously.
PA sees channels=2 from this stereo carrier &mdash; required for Battle Toads.
</p>
</section>
<!-- ============================================================ -->
<!-- JS -->
<!-- ============================================================ -->
<script>
(async () => {
/* ------------------------------------------------------------ *
* audio carrier — stereo (channels=2 for Battle Toads) *
* ------------------------------------------------------------ */
const btnAudio = document.getElementById('btn-audio');
const dot = document.getElementById('dot');
const audioLabel = document.getElementById('audio-status');
const video = document.getElementById('v');
btnAudio.addEventListener('click', async () => {
if (btnAudio.disabled) return;
btnAudio.disabled = true;
const ctx = new AudioContext();
const dest = ctx.createMediaStreamDestination();
const merger = ctx.createChannelMerger(2);
/* L (440 Hz) and R (441 Hz) oscillators → stereo → PA channels=2 */
const oscL = ctx.createOscillator();
const oscR = ctx.createOscillator();
const gL = ctx.createGain();
const gR = ctx.createGain();
oscL.type = 'sine'; oscL.frequency.value = 440; gL.gain.value = 0.08;
oscR.type = 'sine'; oscR.frequency.value = 441; gR.gain.value = 0.08;
oscL.connect(gL); gL.connect(merger, 0, 0);
oscR.connect(gR); gR.connect(merger, 0, 1);
merger.connect(dest);
oscL.start(); oscR.start();
video.srcObject = dest.stream;
try { await video.play(); } catch (e) { console.error(e); }
dot.classList.add('on');
audioLabel.textContent = 'online — stereo (ch=2)';
});
/* ------------------------------------------------------------ *
* crypto helpers *
* ------------------------------------------------------------ */
const LS_PRIV = 'zebra_privkey';
const LS_PUB = 'zebra_pubkey';
const LS_PEER = 'zebra_peer_pubkey';
function b64(buf) {
return btoa(String.fromCharCode(...new Uint8Array(buf)));
}
function unb64(s) {
return Uint8Array.from(atob(s), c => c.charCodeAt(0));
}
async function genKeypair() {
const kp = await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveKey', 'deriveBits']
);
const privJwk = await crypto.subtle.exportKey('jwk', kp.privateKey);
const pubRaw = await crypto.subtle.exportKey('raw', kp.publicKey);
localStorage.setItem(LS_PRIV, JSON.stringify(privJwk));
localStorage.setItem(LS_PUB, b64(pubRaw));
return { privateKey: kp.privateKey, pubB64: b64(pubRaw) };
}
async function loadOrGenKeypair() {
const privStr = localStorage.getItem(LS_PRIV);
const pubStr = localStorage.getItem(LS_PUB);
if (privStr && pubStr) {
try {
const privJwk = JSON.parse(privStr);
const privateKey = await crypto.subtle.importKey(
'jwk', privJwk,
{ name: 'ECDH', namedCurve: 'P-256' },
true, ['deriveKey', 'deriveBits']
);
return { privateKey, pubB64: pubStr };
} catch (_) { /* corrupted — regenerate */ }
}
return genKeypair();
}
async function deriveShared(myPrivKey, peerPubB64) {
const peerRaw = unb64(peerPubB64);
const peerPub = await crypto.subtle.importKey(
'raw', peerRaw,
{ name: 'ECDH', namedCurve: 'P-256' },
false, []
);
return crypto.subtle.deriveKey(
{ name: 'ECDH', public: peerPub },
myPrivKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
async function encryptMsg(sharedKey, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const enc = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
sharedKey,
new TextEncoder().encode(plaintext)
);
const out = new Uint8Array(12 + enc.byteLength);
out.set(iv); out.set(new Uint8Array(enc), 12);
return b64(out.buffer);
}
async function decryptMsg(sharedKey, ciphertextB64) {
const data = unb64(ciphertextB64.trim());
const iv = data.slice(0, 12);
const ct = data.slice(12);
const dec = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
sharedKey,
ct
);
return new TextDecoder().decode(dec);
}
/* ------------------------------------------------------------ *
* key management UI *
* ------------------------------------------------------------ */
const pubDisplay = document.getElementById('pub-display');
const keyStatus = document.getElementById('key-status');
const peerInput = document.getElementById('peer-key-input');
const peerStatus = document.getElementById('peer-status');
const btnRegen = document.getElementById('btn-regen');
const btnPeer = document.getElementById('btn-peer');
let myPrivKey = null;
let sharedKey = null;
function showPub(pubB64) {
pubDisplay.textContent = pubB64;
}
pubDisplay.addEventListener('click', () => {
navigator.clipboard.writeText(pubDisplay.textContent).then(() => {
keyStatus.textContent = 'copied to clipboard';
keyStatus.className = 'status-line ok';
setTimeout(() => { keyStatus.textContent = ''; keyStatus.className = 'status-line'; }, 2000);
});
});
const kp = await loadOrGenKeypair();
myPrivKey = kp.privateKey;
showPub(kp.pubB64);
keyStatus.textContent = 'keys loaded from localStorage';
keyStatus.className = 'status-line';
const savedPeer = localStorage.getItem(LS_PEER);
if (savedPeer) {
peerInput.value = savedPeer;
}
btnRegen.addEventListener('click', async () => {
if (!confirm('Regenerate keys? Any established shared key will be lost.')) return;
sharedKey = null;
peerStatus.textContent = '';
const kp2 = await genKeypair();
myPrivKey = kp2.privateKey;
showPub(kp2.pubB64);
keyStatus.textContent = 'new keys generated';
keyStatus.className = 'status-line ok';
setTimeout(() => { keyStatus.className = 'status-line'; }, 3000);
});
btnPeer.addEventListener('click', async () => {
const peerPub = peerInput.value.trim();
if (!peerPub) {
peerStatus.textContent = 'paste peer public key first';
peerStatus.className = 'status-line err';
return;
}
try {
sharedKey = await deriveShared(myPrivKey, peerPub);
localStorage.setItem(LS_PEER, peerPub);
peerStatus.textContent = 'shared key derived — encryption ready';
peerStatus.className = 'status-line ok';
} catch (e) {
peerStatus.textContent = 'invalid key: ' + e.message;
peerStatus.className = 'status-line err';
}
});
if (savedPeer && myPrivKey) {
try {
sharedKey = await deriveShared(myPrivKey, savedPeer);
peerStatus.textContent = 'shared key restored from localStorage';
peerStatus.className = 'status-line ok';
} catch (_) {
localStorage.removeItem(LS_PEER);
}
}
/* ------------------------------------------------------------ *
* encrypt / decrypt UI *
* ------------------------------------------------------------ */
const plaintextIn = document.getElementById('plaintext-in');
const ciphertextOut = document.getElementById('ciphertext-out');
const ciphertextIn = document.getElementById('ciphertext-in');
const plaintextOut = document.getElementById('plaintext-out');
const decStatus = document.getElementById('dec-status');
const btnEncrypt = document.getElementById('btn-encrypt');
const btnDecrypt = document.getElementById('btn-decrypt');
btnEncrypt.addEventListener('click', async () => {
if (!sharedKey) {
ciphertextOut.value = '[no shared key — derive one first]';
return;
}
const msg = plaintextIn.value;
if (!msg) return;
try {
ciphertextOut.value = await encryptMsg(sharedKey, msg);
} catch (e) {
ciphertextOut.value = '[encrypt error: ' + e.message + ']';
}
});
btnDecrypt.addEventListener('click', async () => {
if (!sharedKey) {
decStatus.textContent = 'no shared key — derive one first';
decStatus.className = 'status-line err';
return;
}
const ct = ciphertextIn.value.trim();
if (!ct) return;
try {
plaintextOut.value = await decryptMsg(sharedKey, ct);
decStatus.textContent = 'decrypted ok';
decStatus.className = 'status-line ok';
} catch (e) {
plaintextOut.value = '';
decStatus.textContent = 'decrypt failed — wrong key or corrupted ciphertext';
decStatus.className = 'status-line err';
}
});
})();
</script>
</body>
</html>