feat: bluetooth and magnetometer channels — two more real instruments

Both measure genuine physics rather than dressing up a random number.

Bluetooth (lib/bluetooth.ts): BLE advertises in the 2.4GHz ISM band, and
the human body is mostly water, which absorbs 2.4GHz strongly — the same
physics that makes a microwave work and that degrades your wifi when
someone stands between you and the router. So RSSI genuinely drops when a
body crosses the path. That makes signal-strength variance a real,
physically-grounded movement signal. The module reports exactly that and
nothing more: it never claims a drop *is* a presence, only that the field
changed. Threshold is 6dB — above the 2-4dB of idle multipath wander, and
inside the 3-10dB a real body actually causes.

Magnetometer (lib/magnetometer.ts): the existing EMF mode infers field
disturbance from DeviceMotion/DeviceOrientation, which is a real
measurement but measures *movement*, not magnetism — a phone sitting still
beside a running motor reads nothing. This reads the actual magnetometer,
so the EMF meter measures what an EMF meter is supposed to. Real
ghost-hunting EMF meters are just magnetometers, and the spikes they pick
up come from mains wiring, motors and moving ferrous mass — all of which
this picks up, for the same real reasons. 3uT threshold clears the ~0.5-1uT
sensor noise while still catching household sources. Earth's constant
25-65uT background is explicitly what the rolling baseline exists to
subtract.

Both are additive: neither replaces the existing motion-based EMF, which
stays the fallback because it works on iOS where neither of these do
(no Web Bluetooth, no Generic Sensor API in any iOS browser). Both reuse
the time-aware EMA baseline shape from coldSpot.ts, since advertisement
and sensor intervals are irregular and a fixed per-sample alpha would
weight a burst and a long gap identically.

Web Bluetooth types are declared locally rather than pulling in
@types/web-bluetooth for three shapes — same approach lib/emf.ts already
takes with its non-standard sensor types.

Also renders unprompted 'manifest' utterances as an intrusion: violet edge,
full opacity (unlike the faded ambient/fragment murmurs), and a brief
blur-in. The unsettling part is that it is perfectly clear and completely
unbidden.

355 frontend tests pass (26 new); i18n parity gate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Indiana
2026-07-28 03:46:48 +00:00
parent bbcbaa0a36
commit 30694a954a
7 changed files with 944 additions and 1 deletions

View File

@@ -1059,6 +1059,42 @@
opacity: 0.75;
}
/* Unprompted speech: nobody asked for this. Treated as an intrusion
rather than a reply — a violet edge marks it as arriving from outside
the conversation, and it sits at full opacity (unlike the deliberately
faded ambient/fragment murmurs) because the unsettling part is that it
is perfectly clear and completely unbidden. */
.tx-utterance.kind-manifest {
border-left: 2px solid rgba(178, 107, 255, 0.6);
padding-left: 0.6rem;
margin-left: -0.2rem;
}
.tx-utterance.kind-manifest .tx-text {
color: #ecdcff;
text-shadow: 0 0 9px rgba(178, 107, 255, 0.45);
animation: manifestArrive 620ms ease-out both;
}
@keyframes manifestArrive {
from {
opacity: 0;
transform: translateX(-5px);
filter: blur(2.5px);
}
to {
opacity: 1;
transform: none;
filter: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tx-utterance.kind-manifest .tx-text {
animation: none;
}
}
.tx-utterance.speaking .tx-speaker {
animation: flickerAnim 1.2s linear infinite;
}