Commit graph

3 commits

Author SHA1 Message Date
Viktor Barzin
147a8cff40 Restore f1-stream stack — undo accidental bundling into 63fe7d2b
Commit 63fe7d2b (fan-control) was made with a bare `git commit` in the
shared infra working tree and inadvertently swept in a parallel session's
staged f1-stream-extraction work (main.tf repoint, ~48 files/ removals,
ci-cd.md + .claude docs, two extraction plan docs).

This returns every f1-stream-related path to its pre-63fe7d2b state
(3493c347) so that extraction can be committed cleanly by its own
session. The fan-control files added in 63fe7d2b are untouched.

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:19:12 +00:00
Viktor Barzin
90ad6b9125 fan-control: presence-aware IPMI fan curve for the R730 PVE host
The iDRAC stock curve runs the CPU at ~72°C on the 7080 RPM floor even
under load (optimises for quiet, not cool). Add a bash daemon + systemd
unit that drives the chassis fans from CPU temp on two curves, picked by
garage occupancy (the server is in the garage): COOL when empty
(measured ~58-65°C under load), QUIET near the silent floor when the
ha-sofia garage door shows someone is there (open, or <15min since last
activity).

Manual fan mode is backstopped: bash EXIT trap + systemd ExecStopPost
hand fans back to Dell auto on stop/crash; CPU>=83°C or repeated IPMI
failures do the same. Pushgateway metrics (job=fan_control). 36 unit
tests cover the pure curve/hysteresis/presence/parse logic; DRY_RUN +
RUN_ONCE for integration checks. Deployed and verified on 192.168.1.127
(CPU 70->58°C in cool mode, hysteresis stepping confirmed).

Design:  docs/plans/2026-06-04-pve-fan-control-design.md
Runbook: docs/runbooks/fan-control.md

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:19:11 +00:00
Viktor Barzin
92474254e6 f1-stream: hmembeds offline decoder — reverse-engineered the JW Player trap
Four-agent parallel investigation finally pinned down what's happening
with the hmembeds.one streams. The TL;DR is unexpected: there is no
fingerprint check, no decoder failure, no broken JS — the obfuscated
decoder is trivial to reproduce, but the upstream origin is dead.

Findings (saved at /tmp/jwre/{findings.md, blob-analysis.md,
fingerprint-gap.md, trace-summary.md}):

1. **The "ZpQw9XkLmN8c3vR3" blob is decoy.** It's an Adcash adblock-
   bypass config — not the stream URL. The actual stream URL is in a
   different inline `<script>` block of the embed HTML.

2. **The real decoder is base64 + XOR with a hardcoded key**, the key
   appears literally in the HTML (e.g. `var k="bux7ver6mow4trh1"`).
   No browser-derived inputs. We can run it in Python in 50µs.

3. **The decoded URL is JWT-bound to /24 of the requestor's IP**. JWT
   payload: `{stream, ip:"176.12.22.0/24", session_id, exp}`. From our
   cluster (egress 176.12.22.76) the JWT IP-binding is satisfied.

4. **The origin still returns 404 (GET) / 403 (HEAD).** Tested both
   curated embeds (Sky F1 888520f3..., DAZN F1 fc3a5463...) — same
   404. Origin landing page (`/`) returns 200, so the host is up;
   the `/sec/<JWT>/<embed_id>.m3u8` endpoint specifically refuses.

5. **No fingerprint surface trips this.** Runtime trace via
   chrome-service hooks confirmed: decoder reads navigator.userAgent
   (heavy), screen dimensions, and a single WebGL getParameter call.
   No canvas, audio, fonts, fetch-to-fingerprint-API. JW Player setup
   is given a valid file URL — the playlist stays empty because JW
   can't fetch the manifest from the (dead) origin.

Verdict: **the legacy curated hmembeds embeds (`888520f3...` Sky F1,
`fc3a5463...` DAZN F1) are upstream-dead.** No browser-side fix is
possible. The community uses these IDs as "24/7 channels" but they're
in a perpetually-offline state right now.

This commit ships the offline decoder anyway, registered as a new
extractor. Two reasons:
- If those origins come back online, no code change needed.
- Future curated hmembeds IDs (added by hand or discovered via
  subreddit posts) will resolve through the same path.

Files added: `extractors/hmembeds.py` (~120 lines incl. the decoder
and a `decode_embed(html) -> str | None` helper that's reusable).
Registered in `__init__.py`. The existing CuratedExtractor stays
disabled; this replaces its mechanism with one that can absorb new
embed IDs without code changes.

Bonus from the agent work:
- Confirmed our stealth.js is sufficient — the runtime trace showed
  the decoder reads only the surfaces we already cover.
- Identified ~10 fingerprint surfaces we don't spoof (platform,
  userAgentData, hardwareConcurrency, deviceMemory, timezone,
  AudioContext, ICE candidates) but proved they're not what's
  blocking us, so no change needed for now.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 23:47:25 +00:00