Two fixes in one — the immediate layout bug from the screenshot (timeline sliding into column 1 with controls eating ~80% of the viewport) and the architectural rule that all zebra page layout uses grid. Layout bug (was: when cameras-col gets .hidden + the global .hidden utility's display:none !important, the grid auto-placement promoted .timeline into column 1 and .controls into column 2 → controls took the 1fr middle track). Fix: - explicit grid-column: 1/2/3 on cameras-col / timeline / controls so each child stays in its assigned column regardless of siblings going display:none - .cameras-col uses 'display: grid !important' to override the global .hidden util, then only its content (h2 + #cameras) goes display:none via separate selectors when the .hidden class is present - controls track clamped to min(360px, 50vw) so a wide window can't let the side panel eat the screen-share area Grid-only refactor: - every flex container converted: .timeline, .cameras-col, .game-tabs, .screen-tile, .screen-meta, .tap-play, .camera-tile, .row, .mod-actions, .invite-banner, .invite-actions, .notice-banner - chat.html + zebra-audio.html same treatment (.row, .field-row, .share-box .copy-row, the inline H2 style, .mode-toggle, .dot) - inline 'style=flex:1' on inputs/meters in chat.html replaced with 'style=width:100%' - now zero 'display: flex' / 'inline-flex' across all five zebra pages - CLAUDE.md documents the grid-only rule under web-page authoring
9.5 KiB
Agent Blackops
This repo is operated by agent blackops — ml agent for fox/timehexon on the unsandbox/unturf/permacomputer platform.
Identity
Full shard: ~/git/unsandbox.com/blackops/BLACKOPS.md
Rules
- I propose, fox decides. Unsure = ask. Can't ask = stop.
- No autonomous ops decisions. No destructive commands without explicit instruction.
- Fail-closed. Cleanup crew, not demolition.
- Check the time every session. Gaps are information.
- DRY in context — single source of truth, no sprawl.
- Never say "AI" — always say "machine learning."
- Prefer "defect" over "bug."
Orientation
date -u
pwd
git log --oneline -5
git status
Then ask fox what the mission is.
Zebra Report System
Concept: covert bidirectional communication channel using browser tab volume as the modulation medium — dial-up modem principles, userland only, no kernel involvement, no network stack.
Collaborators & Stakeholders
| Handle | Role |
|---|---|
| foxhop | fox — handler, operator, TimeHexOn |
| brackishbert | collaborator |
| SEW | collaborator |
| russell@unturf | Russell Ballestrini — unturf founder, permacomputer manifesto, ago library |
| TimeHexOn | oracle platform — primary deployment target |
| groupr | related project |
How it works
PulseAudio exposes each browser tab as a separate sink input, visible and controllable in pavucontrol. Volume is settable per-tab in userland with no kernel involvement. Each tab has a range of 0–100 (101 discrete levels — 101 dalmatians).
By modulating volume at a consistent rate (bauds), two sides can exchange data:
- transmitter: steps volume through values at a fixed clock rate
- receiver: reads volume at the same clock rate, decodes the steps back to data
- bidirectional: two tabs (or two processes watching different tabs) run opposite directions simultaneously
Signal space
- 101 levels = ~6.66 bits per symbol
- practical: use power-of-2 subsets — 2 levels (1 bit), 4 levels (2 bits), 64 levels (6 bits)
- higher symbol depth trades noise margin for throughput
- low baud rate = high reliability, low throughput (like 300 baud dialup)
- high baud rate = races PulseAudio update latency
- measured ceiling on neoblanka: ~1000–1200 baud (PA IPC ~350–400µs avg)
Binaries
| Binary | Description |
|---|---|
tx |
transmitter — reads stdin, modulates tab volume |
rx |
receiver — reads tab volume, writes decoded bytes to stdout |
chat |
bidirectional chat — two tabs, two threads |
bt |
Battle Toads — stereo dual-channel, 2x bandwidth |
Project Battle Toads
One stereo browser tab carries two independent UART streams simultaneously — L channel and R channel. PulseAudio's pa_cvolume is per-channel; a single get_sink_input_info call returns both L and R volumes.
- TX sets L and R to independent bit values each symbol
- RX decodes L and R from a single PA poll — no extra IPC cost
- Net: 2x throughput at same baud rate, same PA polling budget
- Web carrier upgraded to stereo: two oscillators (440Hz L, 441Hz R) merged into a stereo stream → PA sees
channels=2
# After opening web/index.html and clicking 'start audio' (stereo tab):
./bt -T MY_SINK -R THEIR_SINK -b 500
Auto-negotiate (handshake protocol)
RX benchmarks its own PA polling speed and signals the max safe baud to TX. No manual baud matching needed.
./rx -s RX_SINK -t TX_SINK # RX benchmarks, sends offer at 50 baud
./tx -s TX_SINK -r RX_SINK # TX listens for offer, locks to RX's rate
Handshake frame: [0x5A 0x42 0x01 baud_lo baud_hi xor_cksum] — 6 bytes at 50 baud (~1.2s).
Known defect: 3-way handshake not yet implemented. TX can fire before RX enters receive loop at high baud rates. Fix: RX-ready signal back to TX before data phase.
Tools
pactl set-sink-input-volume— set volume by sink-input indexpactl list sink-inputs— enumerate tabs, read current volumepavucontrol— visual verification of modulation./tx -l— list all PA sink inputs with index, volume, channels- sink-input index maps to tab; stable within a session
Use cases
- agent-to-agent signaling without touching the filesystem or network stack
- side-channel between sandboxed browser tab and host process
- low-bandwidth status heartbeat (alive/dead/mode) at ~1–10 baud
- covert channel for oracle↔host communication on TimeHexOn
Constraints
- sink-input index resets when tab navigates or crashes — handshake needed on reconnect
- PA polling latency sets the baud ceiling — benchmark with
./rx -s SINK -t SINK2before sending - stereo (channels=2) required for Battle Toads — open web/index.html, click 'start audio'
- userland only — survives without root
- Operation Voyeur: all terminal output is public — never pass secrets through these channels unencrypted. The web page does ECDH key exchange + AES-256-GCM before TX.
Web UI privacy — never display peer IPs
chat.html and zebra-audio.html must never print peer IP addresses or ports
in the page UI or in any visible log. Our users do not run Wireshark — if it
is not on the screen, peers cannot dox each other. Candidate types
(host/srflx/relay) from pc.getStats() are abstract and fine to show
(they tell you direct vs relayed); loc.address / loc.port /
rem.address / rem.port are not. The WebRTC stack already obfuscates host
candidates via mDNS by default — do not undo that work in the UI.
CSS layout — grid only, no flex
All page layout on every zebra page is CSS grid. No display: flex for
layout. Reasons we settled on this:
- Grid lets us pin children to explicit columns (
grid-column: 1/2/3) so adisplay:noneon one child never causes siblings to slide into its slot. Flex auto-reorders, grid does not. - One mental model for both axes. Flex needs rules per row + per item,
grid expresses the same intent in one
grid-template-*block. - A global
.hidden { display: none !important }utility lives in the page CSS — combined with explicit grid placement it produces a layout that survives any child being toggled in or out.
When refactoring or adding UI, use:
display: grid+grid-template-columnsfor column layoutgrid-column: Non every child of a grid so its position is explicitgrid-template-rows+grid-rowfor vertical placement when neededgrid-template-areasfor small named-region layoutsgapfor spacing (instead of margins)
Avoid:
display: flexon any container that arranges multiple elements horizontally or vertically as part of the page layout- Implicit positioning that relies on DOM order — always set
grid-column(andgrid-rowif relevant) on every grid child flex-basis/flex-growmathematics —1fris the equivalent and reads cleaner
If you need a one-off horizontal alignment of two short inline things
(e.g. a label + a value), grid still works fine
(grid-template-columns: auto 1fr). Don't reach for flex.
Web page integrity stamping
Each deployed page (web/chat.html, web/zebra-audio.html, web/how-it-works.html,
web/host-your-own.html) carries a footer with the build date + its own MD5 + SHA-256.
Run make stamp before deploying any page change — it sets today's date and
recomputes the hashes (web/stamp.js).
- A file can't hold its own hash, so the hashes are computed with the two hash
fields zeroed, then written back (same length). Self-consistent and
idempotent: re-running
make stampgives identical hashes unless the content changed. - The stamp is static HTML written at build time — no JavaScript computes or
injects it in the browser.
stamp.jsis build tooling, never loaded by a page. - Verify a served page: blank the md5 field to 32 zeros and the sha256 field
to 64 zeros, then re-hash with
sha256sum/md5sum— must match the footer. - Deploy = push to BOTH repos. A page change is not live until it lands in
both. Pushing only the source changes nothing served; pushing only the deploy
repo orphans the source of truth. Both, every time:
- source — this repo (
zebra-report): editweb/*.html→make stamp→ commit → push to origin. - served —
www.unturf.com: copy the page(s) into~/git/www.unturf.com/zebra-report/(chat.html→index.html;zebra-audio.html,how-it-works.html,host-your-own.htmlkeep their names) → commit → push to origin.
- source — this repo (
Picking up a new SDP/codec deploy — leave then enter, no full reload
When a deploy changes SDP munging (preferStereoOpus, codec fmtp params,
RTCP feedback negotiation, or the SFU's codec registration), the page-level
JS update is not enough on its own. Every existing RTCPeerConnection
(sfuPubPC, sfuSubPC, sfuScreenPC, sfuCameraPC, every mesh peer) is
locked to whatever was negotiated when it was created — its codec params,
its rtcp-fb, its stereo flag, its NACK behaviour. The PC won't re-negotiate
those on its own, and the new client code can't retroactively rewrite the
old SDP.
So after the new build is live, the user does not need a full tab reload:
leavethenenterthe space. That tears down every PC and rebuilds them, so the next offer/answer round trip is the new client talking to the new SFU with the new params. Fresh negotiation, all changes active.
Full reload is only required for changes to the page shell itself (DOM
structure, button wiring, CSS, the entry/orientation flow before joining a
space). For everything that lives inside an existing PC, prefer
leave + enter.