Skip to content

Meet Transcriber — Changelog

All notable changes are documented here.

[Unreleased] — Landing Page Debug Cleanup (Tasks #682–#691)

Fixed

Remove Body Preview from Telegram (Task #684–#687)

  • 🔍 Landing page — 0 events found Telegram caption no longer includes Body preview: section
  • Caption is now clean: URL only (🔍 Landing page — 0 events found\nURL: ...)
  • Body text capped at 3000 chars inside page.evaluate to limit CDP transfer size (was unbounded)

Debug Body Text Moved to API Events (Task #684–#687)

  • Landing page body text now logged as type: "debug" event in the API event log
  • Visible in dashboard at https://recorder.nicnames.dev and API at https://api.recorder.nicnames.dev
  • Added export function logDebugEvent(text) to src/notifier.js — wraps private appendEvent('debug', text)
  • Available in default export object for use by other modules

Changed

  • src/scheduler.js — caption cleaned, bodyText bounded at 3000, logDebugEvent called for API storage
  • src/notifier.jslogDebugEvent named export + added to default export

[Unreleased] — /iseeevents Debug Snapshot + Calendar Fallback + FFmpeg Retry (Tasks #550–#628)

Fixed

/iseeevents Debug Snapshot (Tasks #550–#582)

  • /iseeevents Telegram command now triggers full debug snapshot:
  • Navigates to meet.google.com/landing, takes screenshot
  • Sends screenshot to Telegram with 🚨 /iseeevents debug snapshot caption
  • Sends page body text (up to 3800 chars) as follow-up
  • Calls sendErrorNotification → logs type: "error" event in API
  • captureDebugSnapshot() exported from src/scheduler.js
  • globalThis._captureDebugSnapshot registered in src/index.js after browser init
  • /iseeevents handler calls snapshot + appendEvent('error', ...) + replies to user

Notification Dialog Dismiss (Tasks #570–#582)

  • Added dismiss step before candidate scan in fetchMeetLandingEvents
  • Clicks any visible button with dismiss-like text ("not now", "close", "deny", "block", "skip")
  • 600ms pause after dismiss for dialog animation
  • Root cause: "Receive desktop notifications from Meet" dialog covered event cards → 0 candidates found

Timezone Fix (Tasks #562–#568)

  • TZ=America/Chicago added to .env (loaded via --env-file=.env by PM2)
  • Process now runs in CDT — screenshots show consistent Central time

Calendar Event Click Fallback (Tasks #590–#628)

  • chips.forEach now extracts _meetUrl from child anchor inside each chip
  • Previously chips found via data-eventid had no _meetUrl — both primary click AND fallback failed
  • Click re-query selector broadened: added data-eventchip alongside data-eventid/data-eventid-r

FFmpeg PulseAudio Race Condition (Tasks #596–#628)

  • Added retry when FFmpeg exits immediately (PulseAudio sink not ready)
  • On immediate exit: waits 3s, re-runs setupPulseAudio(), retries FFmpeg spawn
  • If retry also fails: throws error (no silent missing audio file)
  • Extracted spawnFfmpeg(outputPath) helper — eliminates duplicated spawn + event handler code

Changed

  • src/scheduler.js — notification dismiss, captureDebugSnapshot export, calendar _meetUrl fallback
  • src/notifier.js — /iseeevents command, help text updated
  • src/index.js — globalThis._captureDebugSnapshot registration
  • src/recorder.js — FFmpeg retry on immediate exit, spawnFfmpeg helper extracted
  • .env — TZ=America/Chicago

[Unreleased] — PulseAudio Startup Fix + Landing Improvements (2025-05-23, Tasks #504–#525)

Fixed

PulseAudio Silent Startup (Task #522)

  • setupPulseAudio() now pre-checks if virtual sink/loopback already exist via pactl list short sinks/modules
  • If they exist: logs Virtual sink already exists / Loopback already exists — no error
  • Connection failure / Connection terminated / initialization failed errors suppressed as info (not errors)
  • Eliminates noisy error log on every bot restart when PulseAudio modules already loaded

Landing Page Date Header Filter (Task #504)

  • Replaced bullet-regex date header filter with titleLine requirement
  • Meeting cards always have a title line (e.g. "Test call"); page clock headers never do — this is the reliable discriminator
  • TZ: 'America/Chicago' added to PM2 ecosystem so Chromium renders meeting times in CDT (user's timezone)
  • Landing 0-events debug message now sends a screenshot + body preview caption (not text-only)
  • Debug body log throttled to once per 15 min via _lastBodyDebug

Changed

  • src/recorder.jssetupPulseAudio() pre-checks before loading PulseAudio modules
  • src/scheduler.jstitleLine requirement replaces bullet regex; 0-events debug sends screenshot; TZ in ecosystem
  • ecosystem.config.cjs — added TZ: 'America/Chicago'

[Unreleased] — Date Header Filter Fix (2025-05-23, Tasks #493–#500)

Fixed

Landing Page — Date Header False Click (Task #495)

  • Bot was clicking page clock elements like "11:27 PM • Thu, Mar 26" as if they were meeting cards
  • Added regex filter in fetchMeetLandingEvents: elements matching HH:MM AM/PM • Word (bullet/dash separator after time) are now skipped
  • Meeting cards are multi-line (time\ntitle); date headers are single-line with a bullet — this distinguishes them reliably
  • Covers separators: , ·, -, ,

File changed: src/scheduler.js

[Unreleased] — Explicit Mark-as-Read API (2025-05-23, Tasks #462–#468)

Added

Explicit Event Acknowledgment Flow

  • GET /api/events?show=new|all&limit=Nshow param (default new); no longer auto-marks as read
  • POST /api/events/read — mark events as read by IDs: {"ids":["uuid",...]}
  • POST /api/events/processing — claim events as in-progress by an agent; keeps them visible in show=new
  • status field on all events: "new" | "processing" | "read"
  • Legacy events (without status) auto-migrated on read via migrateEvent()
  • show field echoed in API response for transparency
  • Typical agent flow: GET → POST /processing (claim) → work → POST /read (done)

Changed

  • GET /api/events no longer auto-marks events as read on retrieval (was: auto-mark on GET)
  • notifier.js: new events written with status: "new" field

[Unreleased] — Status Website Fixes (2025-05-23, Tasks #452–#458)

Fixed

API Domain Separation

  • api.recorder.nicnames.dev now returns JSON only (no HTML)
  • Static file middleware skipped for Host: api.* requests
  • Unknown routes return {"error":"Not found","path":"..."} JSON 404
  • Human dashboard (recorder.nicnames.dev) unaffected — still serves HTML + SPA fallback

Screenshots Now Visible on Dashboard

  • Bot screenshots were stored in /tmp and deleted before the dashboard could serve them
  • notifier.js now calls persistScreenshot() to copy each screenshot to /root/meet-transcriber/data/screenshots/ before the /tmp file is removed
  • Dashboard mediaUrl fields now point to persistent paths that remain available
  • Screenshot directory auto-trimmed to last 50 files
  • /screenshot endpoint serves images from the persistent directory

Added

Automated Error Monitoring Cron

  • OpenClaw cron meet-transcriber:api-monitor registered — fires every 10 minutes
  • Fetches https://api.recorder.nicnames.dev/api/events
  • If hasErrors: true → analyzes errors and escalates to the fix team
  • If clean → silent (HEARTBEAT_OK)
  • Events are marked as read on retrieval, so the cron never sees the same error twice

[Unreleased] — Status Website Addition (2025-05-23)

Added

Status Website — recorder.nicnames.dev (Task #442–#448)

Human Dashboard: https://recorder.nicnames.dev - Dark-themed, mobile-friendly status page - Shows last 20 bot events with colored type badges (error=red, screenshot=green, status=blue, etc.) - Screenshot thumbnails embedded inline - Auto-refreshes every 30 seconds via /api/events/all

Machine-Readable API: https://api.recorder.nicnames.dev

Endpoint Description
GET /health {"status":"ok"} — liveness check
GET /api/events Unread events from last 60 min; marks returned events as read (deduplicated per poll)
GET /api/events/all Last 20 events regardless of read state
GET /screenshot?path=<encoded> Serves screenshot images (path restricted to /tmp and /root/meet-transcriber)

Event JSONL log: /root/meet-transcriber/data/events.jsonl - Every Telegram notification now appended as a JSON line: {"id","ts","type","text","mediaUrl","read"} - Event types: status, error, meeting_found, joined, transcript, screenshot - Auto-trimmed to last 2000 lines when file exceeds 5MB

Infrastructure: - meet-status PM2 process — Node/Express server on port 3456 (/root/meet-transcriber-status/) - Nginx reverse proxy: both domains → 127.0.0.1:3456 - Let's Encrypt SSL active on both domains (auto-renews via certbot)

Changed

  • src/notifier.js: appendEvent() added — called from all 6 send functions to log events to JSONL

[Unreleased] — 2025-05-23

Added

Meet Landing Page Event Detection (Tasks #395–#415)

  • DOM debug dump: when fetchMeetLandingEvents returns 0 events, sends a Telegram text message with full DOM diagnostic (links, headings, body preview). Throttled to once per 15 min.
  • Click-based event extraction: rewrote fetchMeetLandingEvents to interact with Meet landing event cards. Cards have no anchor tags — the bot now uses page.$$() to get live ElementHandle refs, clicks each card, and scans the full page HTML for a meet.google.com/xxx-xxxx-xxx pattern exposed in the in-page detail panel.
  • Dual URL extraction strategy: checks URL bar (Strategy A) and full page HTML scan (Strategy B) after each click.
  • seenMeetCodes dedup: prevents processing the same meeting twice per poll.
  • Escape to close panel between card clicks.

Join Flow Improvements (Tasks #417–#430)

  • Screenshot with joined notification: sendJoinedNotification now sends a photo (with caption) instead of text-only. Screenshot is taken inside the meeting after mic/camera mute.
  • detectInMeeting hardened:
  • Added pre-join text guard: body text containing "join now" / "ask to join" / "ready to join" → return false
  • Added DOM-level join-button guard: if any known join jsname button is still present → return false
  • video[autoplay] (remote stream) required for last-resort fallback instead of plain <video> (which fires on pre-join camera preview)
  • 6-second post-click settle delay before detection loop starts, preventing false-positive detection on pre-join state.
  • Last-resort fallback hardened: requires video[autoplay] + !isPreJoin + !hasJoinBtn — all three conditions must hold.

Error Handling (Tasks #433–#438)

  • Browser crash errors suppressed from Telegram: Protocol error, Connection closed, Target closed, Session closed, Navigation timeout, net::ERR_*, detached, Execution context errors are now logged to PM2 only (not sent to Telegram). These are transient self-recovering failures.
  • Browser recovery attempt on crash: when a CDP crash is detected in runScheduler, calls ensureGoogleSession to attempt reconnect before next poll.
  • Eliminated "Unknown meeting" spam: scheduler-level errors with no active meeting no longer generate confusing Telegram error messages.

Changed

  • fetchMeetLandingEvents (src/scheduler.js): complete rewrite — no longer uses static selectors, uses live ElementHandle click + HTML scan
  • sendJoinedNotification (src/notifier.js): now accepts optional screenshotPath param; sends sendPhoto with Markdown caption when provided
  • joinMeeting (src/joiner.js): returns { success, page, screenshotPath } instead of sending screenshot directly
  • runScheduler (src/index.js): three-tier error classification (login / browser-crash / genuine)

Fixed

  • False-positive join detection: bot was declaring "Successfully joined" while still on the pre-join lobby screen
  • "Unknown meeting" Telegram error notifications during browser crashes
  • Click-based element finding: replaced fragile array-index re-query with live ElementHandle refs from page.$$()

Previous Changes (pre-2025-05-23)

Session & Auth

  • Auto re-login on Meet session expiry (areCookiesValid checks both google.com/account and meet.google.com/landing)
  • /login command with step-by-step Telegram screenshots
  • CAPTCHA handoff flow via Telegram reply
  • Backup code pool for 2FA automation
  • CDP cookie capture/injection to persist auth across browser restarts

Recording & Transcription

  • Audio capture via PulseAudio virtual sink
  • MP3 recording at 64kbps mono
  • OpenAI Whisper transcription
  • Transcript delivered to Telegram on meeting end

Telegram Bot Commands

  • /login — force re-authentication
  • /status — show bot status
  • /join <url> — manual meeting join