Session Closeout: Finance Tracker AF Dedup + Card Closure (2026-05-07)

# Session Closeout: Finance Tracker AF Dedup + Card Closure (2026-05-07)

Fixed several data quality issues in the finance tracker’s card benefits system, all stemming from how the system handles multiple cards under the same card product.

## Problems Fixed

**AF Cross-Attribution:** The annual fee detection was pooling transactions from all sibling accounts (same card product), causing one card’s $895 AF to appear on every Amex Biz Plat card. Fixed by searching the primary account first and only falling back to siblings when the primary has no AF transactions at all.

**$0 AF False Positive:** Chase Freedom Unlimited was showing a stale $95 “Annual Membership Fee” from 2019. The amount proximity filter was failing open for $0 AF cards. Fixed with an early return: no-fee cards never enter AF detection.

**Dell Credit Double Counting:** Amex posts identical statement credits to all cards under one account. Plaid reports each with unique transaction IDs but the same date, amount, and description. Credits were being counted on every Biz Plat ($300/150 instead of per-card amounts). Fixed with signature-based dedup: earlier mapping IDs “own” each unique (date, amount, name) credit.

**Card Closure Detection:** Southwest Priority card was closed but still showing projected AF. Wired up Teller’s account status field and added a manual `isClosed` flag. Detection priority: Teller status > Empower “(X)” marker > manual flag > $0 balance + no recent transactions heuristic.

## Technical Details

8 files changed across the stack: Prisma schema (new fields), API routes (PATCH handler, closure skip), detection logic (signature dedup, primary-first AF search), Teller sync (status field), deploy script (certs exclusion).

Also cleaned up 8 redundant card mappings (32 to 24) and re-seeded stale card product data.

**Incident:** Manual rsync –delete during debugging wiped Teller mTLS certs from the VM. Recovered from privateContext backup. Deploy script now excludes certs/ directory.

## Commits
– `47e2c90` Fix AF cross-attribution, benefit credit dedup, and card closure detection
– `d23fc07` Update context.md and progress.md

All changes deployed and verified on production.

Session Closeout: Streaming Display Race Condition Fix (2026-05-07)

Context

Discord #requests response was cut off: the bot reply started mid-word with the entire beginning lost. Traced to an async race condition in streamingDisplay.js.

Root Cause

In _doEdit(), when freezing a message that exceeded 1800 chars, overflow was computed as a string snapshot before await. During the yield, append() calls added text to accumulatedText (new string via +=). After the yield, accumulatedText = overflow overwrote with the stale snapshot, silently discarding all text appended during the await.

Fix

Trim accumulatedText immediately before the first await (not after), so concurrent append() calls correctly extend the overflow portion. Applied to both _doEdit() and finalize().

Repos

  • centralDiscord: commit 12596ad, deployed to VM (claude-bot)

Duration

~15 minutes

Session Closeout: Discord Slash Commands + Junk Mail Unsub (2026-05-07)

What Was Done

Added Discord slash command support to the centralDiscord bot. All 29 existing bot capabilities are now available as /slash commands usable from any channel, plus a new /unsub command for autonomous junk mail unsubscription.

Slash Command Architecture

  • SlashMessageAdapter wraps Discord interactions to look like Message objects, enabling all existing handlers to work without modification
  • reconstructContent() maps slash command options back to !command text format for the existing dispatcher
  • Guild-scoped registration for instant updates
  • Crossposting to designated channels when invoked elsewhere

New /unsub Command

Autonomous junk mail unsubscription agent accepting photos, URLs, or text descriptions of mailers. Uses browser-agent to navigate unsubscribe pages.

Files

  • src/bot/slashCommands.js (new, 454 lines)
  • src/bot/unsubscribe.js (new, 96 lines)
  • src/bot/commands.js (dispatch table + help)
  • src/bot/index.js (InteractionCreate wiring)

Deployed to VM. Logs confirm: [slash] Registered 29 guild slash commands.

Session Closeout: Trading Agent Auth Fix + Cron Setup (2026-05-07)

Context

User reported zero options trading and zero day trading activity in the #trading Discord channel. The trading agent was deployed on 2026-05-06 with options and day trading support, but no trades were executing.

Root Cause 1: Alpaca Auth Failure

The daytrade daemon imports engine/portfolio.py and engine/executor.py before loading .env. Both modules read Alpaca credentials into module-level constants at import time via os.getenv(), which returned empty strings since .env had not been parsed yet. Every Alpaca API call failed with “You must supply a method of authentication” for roughly 21 hours.

Fix: Moved credential reads from module-level constants into the functions that use them. Credentials are now read at call time, after .env is loaded.

Root Cause 2: Missing Cron Jobs

The fast loop (run.sh every 30 min) and deep loop (run.sh –deep at 8:30 AM and 5:30 PM ET) were never added to the VM crontab. Only PM2 processes were configured. All swing trading strategies never executed autonomously.

Fix: Added 4 cron entries: fast loop every 30 min during market hours, deep loop pre-market and after-hours, Monday through Friday.

Follow-up: Usage Threshold Override

The global usage gate (75%) was blocking the deep loop at 77% usage. Trading is the primary value-producing agent and should keep running while other agents are paused.

Fix: Both run.sh and daytrade/daemon.py now use a 90% threshold instead of the global 75%. They call check-usage.sh –json, parse the max bucket percentage, and compare against TRADING_USAGE_THRESHOLD=90. Each trading run costs about $0.18, so the budget impact is minimal.

Verification

  • Portfolio snapshot: 13 positions, $100k paper account
  • Test order: 1 share SPY buy accepted, then cancelled
  • Daytrade daemon: restarted cleanly, no auth errors
  • Full dry-run: built prompt, gathered data correctly

Pattern

When Python modules read environment variables at module level (os.getenv outside any function), the values are captured at import time. If .env is loaded after the import, the variables will be empty. Always read credentials inside functions that use them when .env loading happens at runtime.

Session Closeout: Trading Agent Options + SPY Day Trading (2026-05-06)

Context

Added two new trading strategies to the autonomous trading agent: options trading (long calls/puts) and SPY intraday day trading with a dedicated 3-minute PM2 daemon loop.

What Was Built

  • Options Reactive (10%): OCC symbol parser, DTE/contract/exposure risk checks, long calls/puts only, 3-45 DTE bounds
  • SPY Day Trading (15%): PM2 daemon at 3-min intervals, bracket orders with server-side stop-loss and dynamic take-profit, EOD auto-liquidation at 3:50 PM ET
  • Risk Engine: Replaced blanket option block with proper checks, added day trade checks (position cap, trade limit, EOD cutoff)
  • Bracket Orders: Every SPY buy includes server-side stop-loss and take-profit. Claude specifies both stop_loss_pct and take_profit_pct per trade based on setup quality.

Architecture: Three Loops

  1. Fast Loop (run.sh, 30m cron): 5 swing strategies including options
  2. Deep Loop (run.sh –deep, 2x/day): Thesis-driven analysis
  3. Day Trade Loop (daytrade/daemon.py, PM2, 3m): SPY intraday only

Strategy Allocations (rebalanced)

news 25%, alt_data 25%, insider 15%, macro 10%, options 10%, day_trade 15%

Key Decisions

  • Options as separate strategy per user request, not integrated into existing ones
  • PM2 daemon over cron for day trading (configurable interval, no granularity issues)
  • Conservative options params to start (long only, 15% max exposure)
  • Dynamic take-profit: Claude sets R:R per trade, defaults to 2x stop if omitted

Open Items

  • Option chain data collector (automated contract discovery)
  • Options sections in main prompt templates
  • Day trade cost monitoring (first week)
  • PDT rule for real money transition

Files

3 commits, 9 files changed, +1,338 lines. All 201 tests passing. Deployed to VM.

Session Closeout: Finance Tracker Middleware Redirect Fix + Deploy Script (2026-05-06)

Context

The finance tracker at /finance was redirecting unauthenticated users to WordPress wp-admin instead of the app login page. Header links appeared to go through wp-admin because the middleware auth redirect was missing the /finance basePath prefix.

Root Cause

The middleware used new URL("/login", req.url) which constructs an absolute URL at /login, ignoring Next.js basePath. When deployed at /finance, this sent users to pezant.ca/login, which WordPress caught and redirected to wp-login.php.

Fix

Changed to req.nextUrl.clone() with .pathname = "/login", which respects the configured basePath and correctly redirects to /finance/login.

Deploy Script

During the fix deployment, rsync --delete overwrote the VM production .env (DATABASE_URL on port 5432) with the local dev config (port 15432), breaking the DB connection. Created scripts/deploy.sh that standardizes the deploy flow with --exclude='.env', .env integrity checks, PM2 restart, and health verification with retries.

Decisions

  • req.nextUrl.clone() over hardcoded path – works in both local dev (no basePath) and production (/finance)
  • Deploy script over documented manual steps – eliminates forgot-a-step errors entirely
  • Config via .deploy.env – gitignored file keeps sensitive hostnames out of public repo

Commits

  • 6b4b9cc – Fix middleware redirect missing basePath
  • 3cf493d – Add deploy script with .env protection and health verification
  • 52b20a3 – Update context.md and progress.md

Key Learning

Next.js middleware redirects behind a basePath must always use req.nextUrl methods, never raw new URL(). The latter ignores basePath and produces bare paths that fall through to whatever else handles the domain root.

Chime on Replit Completion

// ==UserScript==
// @name         Replit Chat Completion Ping (Stop/Working FSM + Airy Harp Chime)
// @namespace    nicholas.tools
// @version      1.4.0
// @description  Chimes when Replit chat finishes. Detects streaming via Stop or "Working..". Uses Sound #4 (Airy harp up-gliss). Rich console logs + control panel (window.ReplitPing).
// @match        https://replit.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
  "use strict";

  /* =========================
   * Config
   * ========================= */
  let DEBUG = true;          // high-level logs (state changes, detections)
  let TRACE_SCAN = false;    // very chatty: log detector scans (toggle at runtime)
  const STABILITY_MS_DEFAULT = 200; // require this much time of "no Stop & no Working" before DONE
  let STABILITY_MS = STABILITY_MS_DEFAULT;
  const POLL_MS = 250;

  // Matchers (case-insensitive)
  const STOP_TEXTS = ["stop"];        // exact match (normalized)
  const WORKING_TOKENS = ["working"]; // starts-with; allows Working., Working.., Working…

  /* =========================
   * Pretty Console Logging
   * ========================= */
  const tag = (lvl) => [
    "%cREPLIT-PING%c " + lvl + "%c",
    "background:#121212;color:#00e5ff;padding:1px 4px;border-radius:3px",
    "color:#999",
    "color:inherit"
  ];
  const cI = (...a) => DEBUG && console.log(...tag("ℹ️"), ...a);
  const cS = (...a) => DEBUG && console.log(...tag("✅"), ...a);
  const cW = (...a) => DEBUG && console.log(...tag("⚠️"), ...a);
  const cE = (...a) => DEBUG && console.log(...tag("⛔"), ...a);
  const cT = (...a) => (DEBUG && TRACE_SCAN) && console.log(...tag("🔎"), ...a);

  const nowStr = () => new Date().toLocaleTimeString();

  /* =========================
   * DOM / Text Helpers
   * ========================= */
  const isEl = (n) => n && n.nodeType === 1;
  const isVisible = (el) => {
    if (!isEl(el)) return false;
    const rect = el.getBoundingClientRect?.();
    if (!rect || rect.width === 0 || rect.height === 0) return false;
    const st = getComputedStyle(el);
    if (st.display === "none" || st.visibility === "hidden" || parseFloat(st.opacity || "1") < 0.05) return false;
    return true;
  };

  const norm = (s) => (s || "")
    .replace(/\u2026/g, "...") // ellipsis → three dots
    .replace(/[.\s]+$/g, "")   // trim trailing dots/spaces
    .trim()
    .toLowerCase();

  function findVisibleEquals(tokens) {
    const hits = [];
    const nodes = document.querySelectorAll("span,button,[role='button'],div");
    let scanned = 0;
    for (const el of nodes) {
      scanned++;
      if (!isVisible(el)) continue;
      const txt = norm(el.textContent || "");
      if (tokens.some(tok => txt === tok)) {
        hits.push(el.closest("button,[role='button']") || el);
      }
    }
    cT(`findVisibleEquals scanned=${scanned} hits=${hits.length} tokens=${tokens.join(",")}`);
    return Array.from(new Set(hits));
  }

  function findVisibleStartsWith(tokens) {
    const hits = [];
    const nodes = document.querySelectorAll("span,div,button,[role='button']");
    let scanned = 0;
    for (const el of nodes) {
      scanned++;
      if (!isVisible(el)) continue;
      const txt = norm(el.textContent || "");
      if (tokens.some(tok => txt.startsWith(tok))) {
        hits.push(el);
      }
    }
    cT(`findVisibleStartsWith scanned=${scanned} hits=${hits.length} tokens=${tokens.join(",")}`);
    return Array.from(new Set(hits));
  }

  const isStopVisible    = () => findVisibleEquals(STOP_TEXTS).length > 0;
  const isWorkingVisible = () => findVisibleStartsWith(WORKING_TOKENS).length > 0;
  const isStreamingNow   = () => isStopVisible() || isWorkingVisible();

  /* =========================
   * SOUND #4 — Airy Harp Up-Gliss (HTMLAudio WAV + WebAudio fallback)
   *  - Four soft “pluck” notes rising: C5 → E5 → G5 → C6
   *  - Triangle oscillators with fast attack & gentle decay
   *  - Subtle global low-pass to keep it airy
   * ========================= */
  function makeReplitChimeWavDataURL() {
    const sr = 44100;
    const N  = Math.floor(sr * 1.05); // ~1.05s buffer
    const data = new Float32Array(N);

    // Pluck events
    const freqs = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
    const starts = [0.00, 0.06, 0.12, 0.18];        // seconds
    const A = 0.008;  // Attack seconds
    const D = 0.22;   // Total pluck duration (~decay to near zero)
    const amp = 0.62; // per-note peak

    const tri = (x) => (2 / Math.PI) * Math.asin(Math.sin(x));

    for (let n = 0; n < freqs.length; n++) {
      const f = freqs[n];
      const startIdx = Math.floor(starts[n] * sr);
      const len = Math.floor(D * sr);
      for (let i = 0; i < len && (startIdx + i) < N; i++) {
        const t = i / sr;
        // Envelope: quick attack → gentle exponential decay
        let env;
        if (t < A) {
          env = t / A;
        } else {
          const tau = 0.10; // decay constant
          env = Math.exp(-(t - A) / tau);
        }
        // Triangle pluck
        const s = tri(2 * Math.PI * f * t);
        data[startIdx + i] += amp * env * s;
      }
    }

    // Simple global low-pass (1st-order IIR) for softness (~4.5 kHz)
    const fc = 4500;
    const alpha = (2 * Math.PI * fc) / (2 * Math.PI * fc + sr);
    let y = 0;
    for (let i = 0; i < N; i++) {
      const x = data[i];
      y = y + alpha * (x - y);
      data[i] = y;
    }

    // Gentle soft clip
    for (let i = 0; i < N; i++) {
      const v = Math.max(-1, Math.min(1, data[i]));
      data[i] = Math.tanh(1.05 * v);
    }

    // Pack to 16-bit PCM WAV (mono)
    const bytes = 44 + N * 2;
    const dv = new DataView(new ArrayBuffer(bytes));
    let off = 0;
    const wStr = (s) => { for (let i = 0; i < s.length; i++) dv.setUint8(off++, s.charCodeAt(i)); };
    const w32 = (u) => { dv.setUint32(off, u, true); off += 4; };
    const w16 = (u) => { dv.setUint16(off, u, true); off += 2; };

    wStr("RIFF"); w32(36 + N * 2); wStr("WAVE");
    wStr("fmt "); w32(16); w16(1); w16(1); w32(sr); w32(sr * 2); w16(2); w16(16);
    wStr("data"); w32(N * 2);
    for (let i = 0; i < N; i++) {
      const v = Math.max(-1, Math.min(1, data[i]));
      dv.setInt16(off, v < 0 ? v * 0x8000 : v * 0x7FFF, true);
      off += 2;
    }

    // Base64 encode
    const u8 = new Uint8Array(dv.buffer);
    let b64 = "";
    for (let i = 0; i < u8.length; i += 0x8000) {
      b64 += btoa(String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000)));
    }
    return `data:audio/wav;base64,${b64}`;
  }

  const CHIME_URL = makeReplitChimeWavDataURL();
  const primeAudioEl = new Audio(CHIME_URL);
  primeAudioEl.preload = "auto";

  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  let ctx;
  const ensureCtx = () => (ctx ||= new AudioCtx());

  async function playChime(reason) {
    // Primary: HTMLAudio
    try {
      const a = primeAudioEl.cloneNode();
      a.volume = 1.0;
      await a.play();
      cS(`🔊 Chime (Airy Harp, HTMLAudio) reason=${reason} @ ${nowStr()}`);
      return;
    } catch (e1) {
      cW("HTMLAudio failed; trying WebAudio (Airy Harp)", e1);
    }

    // Fallback: WebAudio version of Sound #4 (Airy Harp)
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      const c = window.__rp_ctx || new AC();
      window.__rp_ctx = c;
      if (c.state !== "running") await c.resume();

      const t0 = c.currentTime + 0.02;

      // Output chain: low-pass for softness
      const out = c.createGain(); out.gain.setValueAtTime(0.85, t0);
      const lp  = c.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 4500; lp.Q.value = 0.6;
      out.connect(lp); lp.connect(c.destination);

      const pluck = (time, f) => {
        const o = c.createOscillator(); o.type = "triangle"; o.frequency.setValueAtTime(f, time);
        const g = c.createGain();
        // Envelope: fast attack, gentle decay
        g.gain.setValueAtTime(0.0001, time);
        g.gain.exponentialRampToValueAtTime(0.6,  time + 0.008);
        g.gain.exponentialRampToValueAtTime(0.001, time + 0.22);
        o.connect(g); g.connect(out);
        o.start(time); o.stop(time + 0.25);
      };

      const freqs = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
      let cur = t0;
      for (const f of freqs) { pluck(cur, f); cur += 0.06; }

      cS(`🔊 Chime (Airy Harp, WebAudio) reason=${reason} @ ${nowStr()}`);
    } catch (e2) {
      cE("WebAudio play failed", e2);
    }
  }

  // Unlock audio on first interaction/visibility
  const unlock = async () => {
    try { await primeAudioEl.play(); primeAudioEl.pause(); primeAudioEl.currentTime = 0; cI("Audio unlocked via HTMLAudio"); } catch {}
    try { if (AudioCtx) { const c = ensureCtx(); if (c.state !== "running") await c.resume(); cI("AudioContext resumed"); } } catch {}
    window.removeEventListener("pointerdown", unlock, true);
    window.removeEventListener("keydown", unlock, true);
  };
  window.addEventListener("pointerdown", unlock, true);
  window.addEventListener("keydown", unlock, true);
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") unlock(); });

  /* =========================
   * FSM (Stop/Working-driven)
   * ========================= */
  let sid = 0;
  let s = null;
  let pollId = 0;
  let lastStop = false;
  let lastWork = false;

  const STATE = { IDLE: "IDLE", STREAMING: "STREAMING", DONE: "DONE" };

  function startPoll() {
    if (pollId) return;
    pollId = window.setInterval(tick, POLL_MS);
  }
  function stopPoll() {
    if (pollId) { clearInterval(pollId); pollId = 0; }
  }

  function armStreaming(origin) {
    if (s && s.state !== STATE.DONE) return;
    s = { id: ++sid, state: STATE.STREAMING, lastStableStart: 0, sawStreaming: true };
    cI(`▶️ STREAMING s#${s.id} (origin=${origin}) @ ${nowStr()}`);
    startPoll();
  }

  function maybeComplete() {
    if (!s || s.state === STATE.DONE) return;
    const streaming = isStreamingNow();
    const now = performance.now();

    // Visibility transition logs
    const curStop = isStopVisible();
    const curWork = isWorkingVisible();
    if (curStop !== lastStop) {
      cI(`Stop visibility: ${curStop ? "ON" : "OFF"}`);
      lastStop = curStop;
    }
    if (curWork !== lastWork) {
      cI(`Working visibility: ${curWork ? "ON" : "OFF"}`);
      lastWork = curWork;
    }

    if (!streaming) {
      if (!s.lastStableStart) {
        s.lastStableStart = now;
        cI(`⏳ Stability window started (${STABILITY_MS}ms)`);
      }
      const elapsed = now - s.lastStableStart;
      if (elapsed >= STABILITY_MS) {
        s.state = STATE.DONE;
        cS(`DONE s#${s.id} (no Stop & no Working for ${Math.round(elapsed)}ms) @ ${nowStr()}`);
        playChime(`s#${s.id}`);
        stopPoll();
        s = null;
      }
    } else {
      if (s.lastStableStart) cW("Stability reset (streaming reappeared)");
      s.lastStableStart = 0;
    }
  }

  function tick() {
    const streaming = isStreamingNow();
    if (streaming && (!s || s.state === STATE.DONE)) {
      armStreaming("tick");
    }
    if (s && s.state === STATE.STREAMING) {
      maybeComplete();
    }
  }

  /* =========================
   * Observers & Event hooks
   * ========================= */
  const obs = new MutationObserver((mutations) => {
    let nudge = false;
    for (const m of mutations) {
      if (m.type === "childList") {
        for (const n of [...m.addedNodes, ...m.removedNodes]) {
          if (isEl(n)) {
            const txt = norm(n.textContent || "");
            if (STOP_TEXTS.some(s => txt.includes(s)) || WORKING_TOKENS.some(w => txt.startsWith(w))) { nudge = true; break; }
          }
        }
      } else if (m.type === "attributes") {
        const el = m.target;
        if (!isEl(el)) continue;
        const txt = norm(el.textContent || "");
        if (STOP_TEXTS.some(s => txt.includes(s)) || WORKING_TOKENS.some(w => txt.startsWith(w))) { nudge = true; }
      }
      if (nudge) break;
    }
    if (nudge) {
      cT("Mutation nudged tick()");
      tick();
    }
  });

  function start() {
    if (!document.body) {
      document.addEventListener("DOMContentLoaded", start, { once: true });
      return;
    }
    obs.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["class", "style", "aria-hidden"]
    });
    startPoll();
    cI("Armed (Stop/Working FSM). Will chime when both disappear (stable).");
    // Initial state snapshot
    lastStop = isStopVisible();
    lastWork = isWorkingVisible();
    cI(`Initial: Stop=${lastStop} Working=${lastWork}`);
    if (lastStop || lastWork) armStreaming("initial");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start, { once: true });
  } else {
    start();
  }

  /* =========================
   * Runtime Controls (Console)
   * =========================
   * window.ReplitPing.setDebug(true|false)
   * window.ReplitPing.setTrace(true|false)
   * window.ReplitPing.setStability(ms)
   * window.ReplitPing.status()
   * window.ReplitPing.test()
   */
  window.ReplitPing = {
    setDebug(v){ DEBUG = !!v; cI(`DEBUG=${DEBUG}`); return DEBUG; },
    setTrace(v){ TRACE_SCAN = !!v; cI(`TRACE_SCAN=${TRACE_SCAN}`); return TRACE_SCAN; },
    setStability(ms){ STABILITY_MS = Math.max(0, Number(ms)||STABILITY_MS_DEFAULT); cI(`STABILITY_MS=${STABILITY_MS}`); return STABILITY_MS; },
    status(){
      const streaming = isStreamingNow();
      const state = s ? s.state : STATE.IDLE;
      const info = {
        state,
        sessionId: s?.id ?? null,
        stopVisible: isStopVisible(),
        workingVisible: isWorkingVisible(),
        streaming,
        stabilityMs: STABILITY_MS,
        pollActive: !!pollId,
        time: nowStr(),
      };
      cI("Status", info);
      return info;
    },
    async test(){ await playChime("manual-test"); return true; }
  };

})();

A tool for importing from Google Docs that doesn’t suck (Mammoth .docx converter)

I was tired of WordPress’s crappy editor so I decided to write a post in Google Docs instead.

However, when copying the content back in. All the images I carefully added did not carry back over. After trying several terrible options (Seraphinite Post .DOCX Source, WordPress.com for Google Docs, docswrite.com — this didn’t work when I tried but seems to function fine after going back and forth with their CEO) I finally tried out Mammoth and it just works which is great.

  1. Add a post
  2. Look for the Mammoth .docx converter box at the bottom
  3. Upload your file
  4. Wait for it to parse
  5. Click on Insert into editor
  6. And then wait for it to finish adding all the content into the editor (my docx took a few minutes since it had quite a few high resolution images)

Japan Recommendations

A quick note on the below content. All of the things I’ve included I personally enjoyed greatly and would enjoy to anyone who shares my tastes. Have fun in Japan!

Tokyo

Tokyo food

  • L’Effervescence


  • Oniku Karyu
  • Cokuun (Coffee Omakase)
  • I’m Donut ?
  • Pizza Bar


  • Cycle
  • IPPUKU&MATCHA

  • Blue Bottle Cafe (surprisingly good Matcha Latte)

  • Parklet Bakery


  • Iki Expresso


  • Le Petit Mec Hibiya (the best pastries I’ve had in Tokyo)

Tokyo things to do

  • Nezu Museum

  • Hamarikyu Gardens



  • Ueno Park (come at golden hour)



  • Tokyo National Museum
  • Teamlabs Borderless
  • Teamlabs Planets

Kyoto

Kyoto Food

  • Akagakiya

Kyoto things to do

  • Kyoto Golden Temple
  • Otagi Nenbutsuji Temple
  • Arashiyama Bamboo Grove (make sure you hike up into the park by the Bamboo Grove for the Valley View!)
  • Mt Inari (go early and hike to the top)
  • Kiyomizu-dera Temple (this was the most spectacular temple that I visited)

  • Ruriko-in Temple

Osaka

Osaka Food

Osaka Things to do

  • Osaka Castle (go at sunrise, its spectacular!)

Other places

Lake Kawaguchi

Onomichi

  • Shinomani-Kaido

Himeji

  • Himeji Castle



Wakayama

  • Tatago Rock

Nara

  • Nara Park


Nikko

  • Kinfuri Falls


  • Shinkyo Bridge


  • Nikko Tamozawa Imperial Villa Memorial Park




  • Nikkō Tōshogū


Tokyo

Tokyo food

  • L’Effervescence

  • Oniku Karyu
  • Cokuun (Coffee Omakase)
  • I’m Donut ?
  • Pizza Bar

  • Cycle
  • IPPUKU&MATCHA

  • Blue Bottle Cafe (surprisingly good Matcha Latte)

  • Parklet Bakery

  • Iki Expresso

  • Le Petit Mec Hibiya (the best pastries I’ve had in Tokyo)

Tokyo things to do

  • Nezu Museum

  • Hamarikyu Gardens

  • Ueno Park (come at golden hour)

  • Tokyo National Museum
  • Teamlabs Borderless
  • Teamlabs Planets

Kyoto

Kyoto Food

  • Akagakiya

Kyoto things to do

  • Kyoto Golden Temple
  • Otagi Nenbutsuji Temple
  • Arashiyama Bamboo Grove (make sure you hike up into the park by the Bamboo Grove for the Valley View!)
  • Mt Inari (go early and hike to the top)
  • Kiyomizu-dera Temple (this was the most spectacular temple that I visited)

  • Ruriko-in Temple

Osaka

Osaka Food

Osaka Things to do

  • Osaka Castle (go at sunrise, its spectacular!)

Other places

Lake Kawaguchi

Onomichi

  • Shinomani-Kaido

Himeji

  • Himeji Castle

Wakayama

  • Tatago Rock

Nara

  • Nara Park

Nikko

  • Kinfuri Falls

  • Shinkyo Bridge

  • Nikko Tamozawa Imperial Villa Memorial Park

  • Nikkō Tōshogū

Tokyo

Tokyo food

  • L’Effervescence

  • Oniku Karyu
  • Cokuun (Coffee Omakase)
  • I’m Donut ?
  • Pizza Bar

  • Cycle
  • IPPUKU&MATCHA

  • Blue Bottle Cafe (surprisingly good Matcha Latte)

  • Parklet Bakery

  • Iki Expresso

  • Le Petit Mec Hibiya (the best pastries I’ve had in Tokyo)

Tokyo things to do

  • Nezu Museum

  • Hamarikyu Gardens

  • Ueno Park (come at golden hour)

  • Tokyo National Museum
  • Teamlabs Borderless
  • Teamlabs Planets

Kyoto

Kyoto Food

  • Akagakiya

Kyoto things to do

  • Kyoto Golden Temple
  • Otagi Nenbutsuji Temple
  • Arashiyama Bamboo Grove (make sure you hike up into the park by the Bamboo Grove for the Valley View!)
  • Mt Inari (go early and hike to the top)
  • Kiyomizu-dera Temple (this was the most spectacular temple that I visited)

  • Ruriko-in Temple

Osaka

Osaka Food

Osaka Things to do

  • Osaka Castle (go at sunrise, its spectacular!)

Other places

Lake Kawaguchi

Onomichi

  • Shinomani-Kaido

Himeji

  • Himeji Castle

Wakayama

  • Tatago Rock

Nara

  • Nara Park

Nikko

  • Kinfuri Falls

  • Shinkyo Bridge

  • Nikko Tamozawa Imperial Villa Memorial Park

  • Nikkō Tōshogū

ChatGPT Chime on Chat Completion (Tampermonkey Script)

Tampermonkey script

// ==UserScript==
// @name         ChatGPT Completion Ping (Composer FSM, background-safe, no-timeout)
// @namespace    nicholas.tools
// @version      5.4.0
// @description  Chime on completion even when window/tab isn't focused. No timeout; FSM: saw Stop → Stop gone + editor empty. Poll + resilient audio.
// @match        https://chat.openai.com/*
// @match        https://chatgpt.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
  "use strict";

  /* =========================
   * Logging
   * ========================= */
  const DEBUG = true;
  const log = (...a) => DEBUG && console.log("[COMP-PING]", ...a);
  const t = () => new Date().toLocaleTimeString();

  /* =========================
   * Selectors (composer only)
   * ========================= */
  const COMPOSER_EDITABLE    = '#prompt-textarea.ProseMirror[contenteditable="true"]';
  const COMPOSER_FALLBACK_TA = 'textarea[name="prompt-textarea"]';
  const SEND_BTN             = '#composer-submit-button[data-testid="send-button"]';
  const STOP_BTN             = '#composer-submit-button[data-testid="stop-button"]';

  /* =========================
   * Audio: HTMLAudio primary (WAV data URL), WebAudio fallback
   * ========================= */
  function makeChimeWavDataURL() {
    const sr = 44100, dur = 0.99;
    const notes = [
      { f: 987.77, d: 0.22 }, { f: 1318.51, d: 0.22 },
      { f: 1174.66, d: 0.20 }, { f: 1318.51, d: 0.30 },
    ];
    const gap = 0.055, amp = 0.28;
    const N = Math.floor(sr * dur);
    const data = new Float32Array(N).fill(0);
    let t0 = 0;
    for (const { f, d } of notes) {
      const nSamp = Math.floor(d * sr);
      const start = Math.floor(t0 * sr);
      for (let i = 0; i < nSamp && start + i < N; i++) {
        const env = i < 0.01*sr ? i/(0.01*sr) : (i > nSamp-0.03*sr ? Math.max(0, (nSamp - i)/(0.03*sr)) : 1);
        const s = Math.sin(2*Math.PI*f*(i/sr));
        const s2 = Math.sin(2*Math.PI*(f*1.005)*(i/sr)) * 0.6;
        data[start+i] += amp * env * (0.7*s + 0.3*s2);
      }
      t0 += d + gap;
    }
    const pcm = new DataView(new ArrayBuffer(44 + N*2));
    let off = 0;
    const wStr = (s) => { for (let i=0;i<s.length;i++) pcm.setUint8(off++, s.charCodeAt(i)); };
    const w32  = (u) => { pcm.setUint32(off, u, true); off+=4; };
    const w16  = (u) => { pcm.setUint16(off, u, true); off+=2; };
    wStr("RIFF"); w32(36 + N*2); wStr("WAVE");
    wStr("fmt "); w32(16); w16(1); w16(1); w32(sr); w32(sr*2); w16(2); w16(16);
    wStr("data"); w32(N*2);
    for (let i=0;i<N;i++) { const v = Math.max(-1, Math.min(1, data[i])); pcm.setInt16(off, v<0?v*0x8000:v*0x7FFF, true); off+=2; }
    const u8 = new Uint8Array(pcm.buffer);
    const b64 = btoa(String.fromCharCode(...u8));
    return `data:audio/wav;base64,${b64}`;
  }

  const CHIME_URL = makeChimeWavDataURL();
  const primeAudioEl = new Audio(CHIME_URL);
  primeAudioEl.preload = "auto";

  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  let ctx;
  const ensureCtx = () => (ctx ||= new AudioCtx());

  async function playChime(reason) {
    try {
      const a = primeAudioEl.cloneNode();
      a.volume = 1.0;
      await a.play();
      log(`🔊 DONE (HTMLAudio) ${reason} @ ${t()}`);
      return;
    } catch {}
    try {
      const c = ensureCtx();
      if (c.state !== "running") await c.resume();
      const t0 = c.currentTime + 0.02;
      const master = c.createGain(); master.gain.setValueAtTime(0.9, t0); master.connect(c.destination);
      const lp = c.createBiquadFilter(); lp.type="lowpass"; lp.frequency.value=4200; lp.Q.value=0.6; lp.connect(master);
      const delay = c.createDelay(0.5); delay.delayTime.value=0.18;
      const fb = c.createGain(); fb.gain.value=0.22; delay.connect(fb); fb.connect(delay); delay.connect(master);
      const bus = c.createGain(); bus.gain.value=0.85; bus.connect(lp); bus.connect(delay);

      const seq = [
        { f: 987.77, d: 0.22 }, { f: 1318.51, d: 0.22 },
        { f: 1174.66, d: 0.20 }, { f: 1318.51, d: 0.30 },
      ];
      let cur = t0, gap = 0.055;
      for (const {f,d} of seq) {
        const o1=c.createOscillator(), g1=c.createGain(); o1.type="triangle"; o1.frequency.value=f;
        g1.gain.setValueAtTime(0.0001,cur); g1.gain.exponentialRampToValueAtTime(0.6,cur+0.01); g1.gain.exponentialRampToValueAtTime(0.001,cur+d);
        o1.connect(g1); g1.connect(bus); o1.start(cur); o1.stop(cur+d+0.02);

        const o2=c.createOscillator(), g2=c.createGain(); o2.type="sine"; o2.frequency.setValueAtTime(f*1.005,cur);
        g2.gain.setValueAtTime(0.0001,cur); g2.gain.exponentialRampToValueAtTime(0.35,cur+0.012); g2.gain.exponentialRampToValueAtTime(0.001,cur+d);
        o2.connect(g2); g2.connect(bus); o2.start(cur); o2.stop(cur+d+0.02);

        cur += d + gap;
      }
      log(`🔊 DONE (WebAudio) ${reason} @ ${t()}`);
    } catch {}
  }

  // Prime on user interaction
  const unlock = async () => {
    try { await primeAudioEl.play(); primeAudioEl.pause(); primeAudioEl.currentTime = 0; } catch {}
    try { if (AudioCtx) { const c = ensureCtx(); if (c.state !== "running") await c.resume(); } } catch {}
    window.removeEventListener("pointerdown", unlock, true);
    window.removeEventListener("keydown", unlock, true);
  };
  window.addEventListener("pointerdown", unlock, true);
  window.addEventListener("keydown", unlock, true);
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") unlock(); });

  /* =========================
   * Composer helpers
   * ========================= */
  const isEl = (n) => n && n.nodeType === 1;
  const visible = (sel) => { const el = document.querySelector(sel); return !!(el && el.offsetParent !== null); };
  const editorEl = () => document.querySelector(COMPOSER_EDITABLE) || document.querySelector(COMPOSER_FALLBACK_TA) || null;
  function editorEmpty() {
    const el = editorEl();
    if (!el) return true;
    if (el.matches('textarea')) return (el.value || '').replace(/\u200b/g,'').trim().length === 0;
    const txt = (el.textContent || '').replace(/\u200b/g,'').trim();
    return txt.length === 0;
  }
  const isStopVisible = () => visible(STOP_BTN);

  /* =========================
   * FSM + background-safe polling (NO TIMEOUT)
   * ========================= */
  let sid = 0;
  let s = null;
  let pollId = 0;
  const STATE = { IDLE:'IDLE', ARMED:'ARMED', CLEARED:'CLEARED', STREAMING:'STREAMING', DONE:'DONE' };

  function stopPoll() { if (pollId) { clearInterval(pollId); pollId = 0; } }

  function startPoll() {
    stopPoll();
    // steady 250ms poll; browsers may throttle in background which is fine
    pollId = window.setInterval(() => tick(true), 250);
  }

  function cancelSession(reason) {
    if (!s) return;
    log(`CANCEL s#${s.id} (${reason})`);
    stopPoll();
    s = null;
  }

  function arm(reason) {
    // Cancel any previous session (no timeout; avoid multiple active)
    if (s) cancelSession("re-ARM");
    s = {
      id: ++sid,
      state: STATE.ARMED,
      sawStop: false,
      sawCleared: editorEmpty(),
      lastStopGoneAt: 0
    };
    log(`ARM s#${s.id} (${reason}) empty=${s.sawCleared} stop=${isStopVisible()} @ ${t()}`);
    startPoll();
    tick();
  }

  function transition(newState, why) {
    if (!s || s.state === STATE.DONE) return;
    if (s.state !== newState) {
      s.state = newState;
      log(`${newState} s#${s.id} (${why}) empty=${editorEmpty()} stop=${isStopVisible()} @ ${t()}`);
    }
  }

  function evaluate() {
    if (!s || s.state === STATE.DONE) return;

    // Editor cleared after send
    if (!s.sawCleared && editorEmpty()) {
      s.sawCleared = true;
      transition(STATE.CLEARED, "editor cleared");
    }

    // Streaming seen
    if (!s.sawStop && isStopVisible()) {
      s.sawStop = true;
      transition(STATE.STREAMING, "stop visible");
    }

    // Stop disappears
    if (s.sawStop && !isStopVisible() && !s.lastStopGoneAt) {
      s.lastStopGoneAt = performance.now();
      log(`STOP-GONE s#${s.id} (detected)`);
    }

    // Completion: saw Stop once AND Stop gone AND editor empty (150ms stability)
    if (s.sawStop && !isStopVisible() && editorEmpty()) {
      const stable = s.lastStopGoneAt ? (performance.now() - s.lastStopGoneAt) : 999;
      if (stable >= 150) {
        transition(STATE.DONE, "stop gone + editor empty");
        playChime(`s#${s.id}`);
        stopPoll();
        s = null;
      }
    }
  }

  const tick = () => { evaluate(); };

  /* =========================
   * Events & Observers
   * ========================= */
  document.addEventListener("click", (e) => {
    const btn = isEl(e.target) ? e.target.closest(SEND_BTN) : null;
    if (!btn) return;
    arm("send-button click");
  }, true);

  document.addEventListener("keydown", (e) => {
    const ed = isEl(e.target) && (e.target.closest(COMPOSER_EDITABLE) || e.target.closest(COMPOSER_FALLBACK_TA));
    if (!ed) return;
    if (e.key !== "Enter" || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
    if (document.querySelector(SEND_BTN)) arm("keyboard Enter");
  }, true);

  const obs = new MutationObserver((mutations) => {
    if (!s) return;
    for (const m of mutations) {
      if (m.type === "attributes") {
        const el = m.target;
        if (isEl(el) && (el.id === "composer-submit-button" || el.matches(COMPOSER_EDITABLE) || el.matches(COMPOSER_FALLBACK_TA))) {
          tick();
        }
      }
      if (m.type === "childList") {
        for (const n of m.addedNodes) {
          if (!isEl(n)) continue;
          if (n.matches(STOP_BTN) || n.matches(SEND_BTN) ||
              n.querySelector?.(STOP_BTN) || n.querySelector?.(SEND_BTN) ||
              n.matches(COMPOSER_EDITABLE) || n.matches(COMPOSER_FALLBACK_TA) ||
              n.querySelector?.(COMPOSER_EDITABLE) || n.querySelector?.(COMPOSER_FALLBACK_TA)) {
            tick();
            break;
          }
        }
        for (const n of m.removedNodes) {
          if (!isEl(n)) continue;
          if (n.matches(STOP_BTN) || n.matches(SEND_BTN) ||
              n.matches(COMPOSER_EDITABLE) || n.matches(COMPOSER_FALLBACK_TA)) {
            tick();
            break;
          }
        }
      }
      if (m.type === "characterData") tick();
    }
  });

  function start() {
    obs.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["data-testid","id","class","style","contenteditable","value"],
      characterData: true
    });
    log("armed (composer FSM, background-safe, no-timeout). Completes on: saw Stop → Stop gone + editor empty.");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start, { once: true });
  } else {
    start();
  }
})();

Go actual full screen on Simplepractice.com video calls

When I’m on calls with my therapist it has bugged me that I can’t actually go fullscreen with the video so I gave CGPT the code for the page and wrote up a script that expands the fullscreen video to actually be fullscreen.

Use either the console script or Tampermonkey script and try it out for yourself!

Console script

(() => {
  const TOGGLE = 'tm-full-bleed-active';
  const ROOT_SEL = 'div.bbwWj.video';
  const PANEL_SEL = `${ROOT_SEL} > div.mljTO`;
  const STREAM_CONTAINER_SEL = `${ROOT_SEL} .stream-container`;
  const PARTICIPANT_SEL = `${ROOT_SEL} .participant-container`;
  const VIDEO_SEL = `${ROOT_SEL} video`;

  // --- style injection ---
  const css = `
    html.${TOGGLE}, body.${TOGGLE}{margin:0!important;padding:0!important;overflow:hidden!important}
    html.${TOGGLE} ${PANEL_SEL}{
      position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;
      margin:0!important;padding:0!important;border:0!important;background:#000!important;
      z-index:2147483647!important;box-shadow:none!important
    }
    html.${TOGGLE} ${STREAM_CONTAINER_SEL}, html.${TOGGLE} ${PARTICIPANT_SEL}{
      position:absolute!important;inset:0!important;width:100%!important;height:100%!important;
      margin:0!important;padding:0!important;border:0!important;max-width:none!important;max-height:none!important;overflow:hidden!important;background:transparent!important
    }
    html.${TOGGLE} ${VIDEO_SEL}{
      position:absolute!important;inset:0!important;width:100%!important;height:100%!important;
      object-fit:cover!important;object-position:center center!important;display:block!important;background:#000!important;border:0!important;box-shadow:none!important;transform:none!important
    }
    html.${TOGGLE} ${ROOT_SEL} .name, html.${TOGGLE} ${ROOT_SEL} .pin-button, html.${TOGGLE} ${ROOT_SEL} .more-button{
      display:none!important;visibility:hidden!important;pointer-events:none!important
    }
    .tm-fb-btn{
      position:fixed!important;right:14px;bottom:14px;z-index:2147483647!important;
      font:600 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
      padding:10px 12px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;
      border:1px solid rgba(255,255,255,.2);cursor:pointer;user-select:none;backdrop-filter:blur(4px)
    }
    .tm-fb-btn:hover{background:rgba(0,0,0,.75)}
  `;
  let style = document.getElementById('tm-fb-style');
  if (!style) {
    style = document.createElement('style');
    style.id = 'tm-fb-style';
    style.textContent = css;
    document.head.appendChild(style);
  }

  // --- helpers ---
  const $ = (s, r = document) => r.querySelector(s);
  const nap = (ms) => new Promise(r => setTimeout(r, ms));
  const waitFor = async (sel, tries = 200, step = 100) => {
    while (tries-- > 0) { const el = $(sel); if (el) return el; await nap(step); }
    return null;
  };

  // --- state ---
  let btn;

  const isActive = () =>
    document.documentElement.classList.contains(TOGGLE) ||
    document.body.classList.contains(TOGGLE);

  const setActive = (on) => {
    [document.documentElement, document.body].forEach(el => el && el.classList.toggle(TOGGLE, on));
    if (btn) btn.textContent = on ? 'Exit Full-Bleed' : 'Full-Bleed';
  };

  const toggle = () => setActive(!isActive());

  // --- UI button ---
  const mountButton = () => {
    if (btn && document.body.contains(btn)) return btn;
    btn = document.createElement('button');
    btn.className = 'tm-fb-btn';
    btn.type = 'button';
    btn.textContent = isActive() ? 'Exit Full-Bleed' : 'Full-Bleed';
    btn.addEventListener('click', toggle);
    document.body.appendChild(btn);
    return btn;
  };

  // --- key bindings ---
  const bindKeys = () => {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggle(); }
      else if (e.key === 'Escape' && isActive()) { e.preventDefault(); setActive(false); }
    }, { capture: true });
  };

  // --- dblclick on video ---
  const bindVideoDblClick = (v) => {
    if (!v || v.dataset.tmFbBound) return;
    v.addEventListener('dblclick', (e) => { e.preventDefault(); toggle(); }, { capture: true });
    v.dataset.tmFbBound = '1';
  };

  // --- observe dynamic DOM ---
  const observe = () => {
    const mo = new MutationObserver(() => {
      const v = $(VIDEO_SEL);
      if (v) bindVideoDblClick(v);
      if (!btn && document.body) mountButton();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  };

  // --- bootstrap ---
  (async () => {
    await waitFor('body');
    bindKeys();
    observe();
    const v = await waitFor(VIDEO_SEL, 120, 150);
    if (v) bindVideoDblClick(v);
    mountButton();

    // Expose controls for manual use
    window.tmFullBleed = { toggle, on: () => setActive(true), off: () => setActive(false) };
    console.log('[tmFullBleed] Ready. Use tmFullBleed.toggle(), press F, double-click video, or use the button.');
  })();
})();

Tampermonkey script

// ==UserScript==
// @name         Full-Bleed Video (SimplePractice only)
// @namespace    nic.tools.video
// @version      1.0.1
// @description  Force main video to go true full-bleed (edge-to-edge) on SimplePractice video
// @author       you
// @match        https://video.simplepractice.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // ---------- small utils ----------
  const nap = (ms) => new Promise(r => setTimeout(r, ms));
  const q = (sel, root = document) => root.querySelector(sel);

  // Wait for an element or return null after tries*interval
  const waitFor = async (fnOrSel, tries = 200, interval = 100) => {
    while (tries-- > 0) {
      let el = null;
      try { el = typeof fnOrSel === 'function' ? fnOrSel() : q(fnOrSel); } catch {}
      if (el) return el;
      await nap(interval);
    }
    return null;
  };

  // ---------- selectors from provided markup ----------
  const ROOT_SEL = 'div.bbwWj.video';
  const PANEL_SEL = `${ROOT_SEL} > div.mljTO`;
  const STREAM_CONTAINER_SEL = `${ROOT_SEL} .stream-container`;
  const PARTICIPANT_SEL = `${ROOT_SEL} .participant-container`;
  const VIDEO_SEL = `${ROOT_SEL} video`;

  const TOGGLE_CLASS = 'tm-full-bleed-active';

  // ---------- inject styles ----------
  GM_addStyle(`
    /* Full-bleed mode root locks */
    html.${TOGGLE_CLASS}, body.${TOGGLE_CLASS} {
      margin: 0 !important;
      padding: 0 !important;
      overflow: hidden !important;
    }

    /* Pin the main panel to the viewport */
    html.${TOGGLE_CLASS} ${PANEL_SEL} {
      position: fixed !important;
      inset: 0 !important;
      width: 100vw !important;
      height: 100vh !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      background: #000 !important;
      z-index: 2147483647 !important;
      box-shadow: none !important;
    }

    /* Ensure no intermediate wrapper constrains size */
    html.${TOGGLE_CLASS} ${STREAM_CONTAINER_SEL},
    html.${TOGGLE_CLASS} ${PARTICIPANT_SEL} {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      max-width: none !important;
      max-height: none !important;
      overflow: hidden !important;
      background: transparent !important;
    }

    /* Make the video fill and crop (letterboxing avoidance) */
    html.${TOGGLE_CLASS} ${VIDEO_SEL} {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      object-fit: cover !important;
      object-position: center center !important;
      display: block !important;
      background: #000 !important;
      border: 0 !important;
      box-shadow: none !important;
      transform: none !important;
    }

    /* Hide overlays that might intrude (adjust as needed) */
    html.${TOGGLE_CLASS} ${ROOT_SEL} .name,
    html.${TOGGLE_CLASS} ${ROOT_SEL} .pin-button,
    html.${TOGGLE_CLASS} ${ROOT_SEL} .more-button {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
    }

    /* On-screen toggle button */
    .tm-fb-btn {
      position: fixed !important;
      right: 14px;
      bottom: 14px;
      z-index: 2147483647 !important;
      font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
      padding: 10px 12px;
      border-radius: 999px;
      background: rgba(0,0,0,0.6);
      color: #fff;
      border: 1px solid rgba(255,255,255,0.2);
      cursor: pointer;
      user-select: none;
      backdrop-filter: blur(4px);
    }
    .tm-fb-btn:hover { background: rgba(0,0,0,0.75); }
  `);

  // ---------- toggle logic ----------
  const setActive = (on) => {
    const rootList = [document.documentElement, document.body].filter(Boolean);
    rootList.forEach(el => el.classList.toggle(TOGGLE_CLASS, on));
    if (btn) btn.textContent = on ? 'Exit Full-Bleed' : 'Full-Bleed';
  };

  const isActive = () =>
    document.documentElement.classList.contains(TOGGLE_CLASS) ||
    document.body.classList.contains(TOGGLE_CLASS);

  const toggle = () => setActive(!isActive());

  // ---------- button ----------
  let btn;
  const mountButton = () => {
    if (btn && document.body.contains(btn)) return btn;
    btn = document.createElement('button');
    btn.className = 'tm-fb-btn';
    btn.type = 'button';
    btn.textContent = isActive() ? 'Exit Full-Bleed (f)' : 'Full-Bleed (f)';
    btn.addEventListener('click', toggle);
    document.body.appendChild(btn);
    return btn;
  };

  // ---------- event bindings ----------
  const bindKeys = () => {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'f' || e.key === 'F') {
        e.preventDefault();
        toggle();
      } else if (e.key === 'Escape' && isActive()) {
        e.preventDefault();
        setActive(false);
      }
    }, { capture: true });
  };

  const bindVideoDoubleClick = (video) => {
    if (!video || video.dataset.tmFbBound) return;
    video.addEventListener('dblclick', (e) => {
      e.preventDefault();
      toggle();
    }, { capture: true });
    video.dataset.tmFbBound = '1';
  };

  // ---------- observer to reapply on dynamic pages ----------
  const observe = () => {
    const mo = new MutationObserver(() => {
      const video = q(VIDEO_SEL);
      if (video) bindVideoDoubleClick(video);
      if (!btn && document.body) mountButton();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true, attributes: false });
  };

  // ---------- bootstrap ----------
  (async () => {
    await waitFor(() => document.body);
    bindKeys();
    observe();

    const video = await waitFor(VIDEO_SEL, 120, 150);
    if (video) bindVideoDoubleClick(video);

    mountButton();

    // Optional: expose console controls
    window.tmFullBleed = { toggle, on: () => setActive(true), off: () => setActive(false) };
    console.log('[tmFullBleed] Ready on SimplePractice. Use tmFullBleed.toggle(), press F, double-click video, or use the button.');
  })();

})();

See my creation conversation with ChatGPT here:

https://chatgpt.com/share/68c89780-f700-8007-916b-2d707b1970a6

Fix for audio cutting out for 1-3 seconds randomly with MSI B650-S Wifi Motherboard

If your sound keeps cutting out seemingly randomly for your wirelessly headset with this motherboard then try plugging it into the other USB slots (not the blue ones, the red ones). It seems to have fixed my issue. Seems the other ones may not have enough power to support the power draw.

Found the fix here: Audio issues MSI Pro B650-S Wifi | MSI Global English Forum

Have you use another headphone to check for the symptom?

I think I found the solution to my issue :

The USB 3.2 Gen 1 ports connect to something called “Hub-1074” and I guess those aren’t good enough for USB headsets. I at first thought that it was an issue with the board because the chipset driver updates reduced the problem, but didn’t fix it. I then tried connecting with a different headset that uses 3.5mm jacks and it worked without issues, so now I have connected my USB headset to one of the USB 3.2 Gen 2 ports that connect to the CPU, and so far I haven’t had any audio issues.