elements with no links, so users could not view plan content.
Solution:
Fixed chat links in cursor.php to point to /projects/chat-transcript.php (correct filename). Migrated chat-transcript.php from SQLite3 to PDO/PostgreSQL (PgClient::connect()), updating table names (cursor_chats, cursor_chat_plans, cursor_chat_deep_analysis) and handling JSONB columns. Created symlink in web public directory.
Created new plan-viewer.php page that reads plan content from cursor_plans table (with disk fallback), renders markdown via Parsedown, and displays metadata. Wrapped plan cards and list items in tags. Created symlink in web public directory.Files affected:
apps/zap-projects/web/cursor.php -- fixed chat hrefs, wrapped plans in links, added display: block to card CSS
apps/zap-projects/web/chat-transcript.php -- full SQLite-to-PostgreSQL migration
apps/zap-projects/web/plan-viewer.php -- new file
web/public/projects/chat-transcript.php -- new symlink
web/public/projects/plan-viewer.php -- new symlinkEnhancement: Lazy-load plans, sticky tabs, plan-viewer back link
Date: 2026-02-19
Problem: Plans loaded all 449+ at once (originally capped at 100, then 500), tab selection reset to Chats on every page refresh, and the plan-viewer "All Plans" back link went to the Chats tab instead of Plans.
Solution:
Added offset-based pagination to loadPlansData() with page size 50 (matching chats). loadMore() branches on currentTab. Filter/search resets planOffset.
Tab selection persisted to localStorage via existing prefs object (prefs.tab). On init, currentTab reads from URL ?tab= param first, then localStorage, then defaults to 'chats'. Tab buttons get correct .active class at startup.
Plan-viewer back link changed to /projects/cursor?tab=plans. cursor.php init reads URLSearchParams to support deep-linking.Files affected:
apps/zap-projects/web/cursor.php -- pagination, sticky tabs, URL param support
apps/zap-projects/web/plan-viewer.php -- back link href---
Bug: Plans not auto-indexed into database when created by Cursor
Date: 2026-02-19
Problem: When Cursor creates a plan file (
.plan.md), it was not being indexed into the PostgreSQL
cursor_plans table. Searching for "vpn pemex" via the MCP
search_all tool or the web UI returned zero results, despite the plan file existing on disk at
/home/jd/.cursor/plans/vpn_vm_for_pemex_086acecd.plan.md. At the time of discovery, 445 plans existed on disk but only 416 were in the database.
Root Cause (3 issues):
File watcher didn't watch plans directory: watch-transcripts.sh only watched agent-transcripts directories, not /home/jd/.cursor/plans/.
Single-chat mode skipped plan indexing: The watcher ran --single-chat which is guarded by if (!$singleChat) at line 695 of ingest-chat-transcripts.php, skipping all plan indexing code.
Search was too literal: list_plans used ILIKE '%entire query%' -- the whole multi-word string had to appear as a contiguous substring. And cursor_chats.fts_vector only indexed first_query + summary, not the full transcript, so terms like "VPN" at position 663K in a transcript were never indexed.Additional issue found during fix: Plan names in the database were corrupted -- many had the full file content (up to 6KB) stored in the name column instead of the short plan name. This was caused by the upsert's ON CONFLICT clause not updating the name column, so stale data from a previous broken ingest persisted.
Solution:
Added --plans-only CLI mode to ingest-chat-transcripts.php for fast plan-only indexing (~3s for 446 plans)
Added /home/jd/.cursor/plans/ to the inotify watch list in watch-transcripts.sh, with .plan.md events triggering --plans-only ingestion (30s debounce)
Added fts_vector (tsvector generated column + GIN index) to cursor_plans table
Expanded cursor_chats.fts_vector to include full_transcript (no size cap -- largest tsvector is ~135KB for a 5.4MB transcript, well within PostgreSQL's 1MB limit)
Updated MCP server and API to use plainto_tsquery FTS instead of ILIKE
Fixed discoverPlans() to parse YAML frontmatter name field, and fixed upsert to update name on conflict
Restarted systemd watcher serviceFiles affected:
scripts/ingest-chat-transcripts.php -- added --plans-only mode, fixed discoverPlans() name extraction, fixed upsert
scripts/watch-transcripts.sh -- added plans directory watching, plan event handling
apps/zap-projects/mcp/server.py -- switched list_plans from ILIKE to FTS
apps/zap-projects/web/api/plans.php -- switched search from ILIKE to FTS
apps/zap-projects/web/cursor.php -- fixed plans list view, added datetime displayLesson learned: File watchers must watch ALL directories where user-facing content is created, not just transcript directories. Search systems should use proper full-text search, not substring matching. And upsert ON CONFLICT clauses should update ALL columns that might change, not just content/timestamps.
---
Bug: Ollama qwen3:30b wedging and JSON parsing failures
Date: 2026-02-18
Problem: During zap-writer currency scanning and fact-checking, local
qwen3:30b on delphi.lan would frequently become unresponsive. The model would accept a request, then produce no output for minutes before timing out. Subsequent requests to Ollama (even simple ones) would also hang, indicating the entire Ollama server was wedged. When responses did arrive, they often contained "thinking" tokens (
...) mixed into what should have been pure JSON output, causing JSON parsing failures in
LLMClient::generateJSON().
Root Cause: The qwen3 series models have a "thinking mode" that generates internal reasoning tokens before the actual response. This is triggered unpredictably and the thinking output can be very long, consuming GPU memory and time. When combined with large context windows (full chapter text), the model would exhaust GPU resources and wedge Ollama entirely.
Solution (multi-pronged):
Restarted Ollama service on delphi.lan via SSH (sudo systemctl restart ollama) to clear wedged state
Created fix-currency-gaps.php with direct curl calls to Ollama API using /no_think prefix in prompts to suppress thinking mode
Increased num_predict parameter to avoid truncated responses
Implemented manual JSON extraction (regex for {...}) as fallback when standard parsing fails
For persistently problematic items, manually populated database entries based on available search results
Switched bibliography ingestion from LLM-based parsing to regex-based ingest-bib-direct.php for reliabilityFiles affected:
apps/zap-writer/bin/fix-currency-gaps.php -- created for retry logic
apps/zap-writer/bin/ingest-bib-direct.php -- created as LLM-free alternative
apps/zap-writer/bin/run-all-engines.php -- added sleep delays between LLM callsLesson learned: Local LLMs on consumer GPUs are unreliable for long batch processing. Always have: (a) a CLI runner that bypasses HTTP timeouts, (b) a non-LLM fallback for structured parsing tasks, (c) manual JSON extraction as a safety net, (d) the ability to restart the LLM server remotely.
---
Bug: kimi-k2.5:cloud rate limiting (HTTP 429)
Date: 2026-02-18
Problem: Cloud-proxied model
kimi-k2.5:cloud on delphi.lan returned HTTP 429 (Too Many Requests) during batch currency scanning, causing all subsequent LLM calls to fail.
Solution: Upgraded to Ollama paid plan, which removes rate limiting for cloud-proxied models. Also switched batch processing to local qwen3:30b to avoid cloud dependencies entirely.
Lesson learned: Cloud-proxied models via Ollama's free tier have aggressive rate limits unsuitable for batch processing. Either use paid plan or prefer local models for batch work.
---
Bug: WireGuard iptables-restore missing in Alpine LXC
Date: 2026-02-18
Problem: When bringing up WireGuard VPN in the Alpine LXC container on phoebe.lan,
wg-quick up failed with
iptables-restore: command not found. The WireGuard interface was created but then immediately torn down because the firewall rules couldn't be applied.
Solution: apk add iptables ip6tables inside the container. Alpine's minimal install doesn't include iptables by default, but WireGuard's wg-quick requires it for its routing table and firewall mark rules.
Files affected: Container setup (not in git -- LXC on phoebe.lan)
---
Bug: WireGuard resolvconf signature mismatch in Alpine LXC
Date: 2026-02-18
Problem: After installing iptables,
wg-quick up still failed because
resolvconf reported a "signature mismatch" on
/etc/resolv.conf and refused to update DNS settings. The WireGuard interface was again created and immediately torn down.
Solution: Bypassed resolvconf entirely by handling DNS in the WireGuard config's PostUp/PostDown hooks:
PostUp = cp /etc/resolv.conf /etc/resolv.conf.bak; printf "nameserver 10.0.0.243\nnameserver 8.8.8.8\n" > /etc/resolv.conf
PostDown = cp /etc/resolv.conf.bak /etc/resolv.conf
---
Bug: PIA addKey endpoint returning "Login failed!"
Date: 2026-02-18
Problem: After generating a valid PIA auth token and WireGuard keypair, the PIA
addKey endpoint at
https://:1337/addKey returned
{"status":"ERROR","message":"Login failed!"}. The same token worked fine for the
generateToken endpoint.
Root Cause (1): The PIA password contains a * character which was being expanded by the shell when passed through SSH heredocs. The token was acquired successfully from orcus but the addKey call was made from inside the container where the password wasn't needed -- the issue was that the addKey endpoint uses a self-signed certificate and requires PIA's own CA certificate for proper TLS validation.
Root Cause (2): The curl call used -k (insecure) which should have worked, but PIA's server requires the --resolve flag to map the hostname to the IP address. Without --resolve, the TLS SNI doesn't match and the server rejects the request.
Solution: Used PIA's official CA certificate (ca.rsa.4096.crt from their manual-connections repo) with --resolve flag:
curl --cacert ca.rsa.4096.crt \
--resolve "mexico409:1337:77.81.142.97" \
-G --data-urlencode "pt=${TOKEN}" --data-urlencode "pubkey=${WG_PUBKEY}" \
"https://mexico409:1337/addKey"
Files affected: /root/scripts/vpn-connect.sh on vpn-fetch.lan container
Lesson learned: PIA's WireGuard registration requires their specific CA cert and hostname resolution. The -k flag is not sufficient. Always use the official manual-connections repo scripts as reference.
---
Bug: Bibliography ingestion too slow via LLM parsing
Date: 2026-02-18
Problem: The initial approach to bibliography ingestion used LLM (
qwen3:30b) to parse each reference from the manuscript text into structured fields (author, title, year, publisher, etc.). With 528 references, this was extremely slow (>10 seconds per reference when it worked) and frequently wedged Ollama completely.
Solution: Created ingest-bib-direct.php which parses references using PHP regex and heuristics:
Detects author names (capitalised words followed by comma/period patterns)
Extracts years (4-digit numbers in parentheses or after commas)
Identifies titles (text in quotes or italics)
Handles URLs and DOIs
Completes 528 references in under 5 seconds vs estimated 90+ minutes via LLMLesson learned: LLMs should not be used for structured parsing of well-formatted text. Regex/heuristic parsers are orders of magnitude faster, more reliable, and don't consume GPU resources. Reserve LLMs for tasks that genuinely require understanding (verification, summarisation, merging).
---
Bug: MCP tools not available to AI agent in SSH remote workspaces
Date: 2026-02-17
Problem: After setting up the zap-projects MCP server and deploying config files, the AI agent in SSH remote workspaces (e.g. philoenic on orcus accessed from paris.lan) reported "I don't see a zap-projects MCP server" despite the Cursor settings UI showing "zap-projects: 7 tools enabled" with a green status dot.
Root Cause (1 -- Windows global config path):
The global MCP config on paris.lan (Windows 11) was placed at %APPDATA%\Cursor\mcp.json (C:\Users\jd\AppData\Roaming\Cursor\mcp.json). This is wrong. Cursor reads global MCP config from %USERPROFILE%\.cursor\mcp.json (C:\Users\jd\.cursor\mcp.json). The %APPDATA%\Cursor\ directory is for Cursor's internal state, not user configs.
Root Cause (2 -- global vs project-level for SSH remotes):
Even after fixing the path, the global config on the Windows client did not make tools available to the AI agent. The reason: for SSH remote workspaces, the AI agent runs on the remote machine (orcus), but the global MCP config starts the MCP server on the local machine (paris.lan). The Cursor UI (which runs on paris.lan) shows the server as connected, but the agent on orcus cannot communicate with it. The settings UI and the agent have different views of the MCP server.
Solution:
Project-level .cursor/mcp.json files at each workspace root on orcus, with a local python3 command that runs the MCP server directly on orcus (where the agent runs). This bypasses the client-server gap entirely.
Files affected:
C:\Users\jd\.cursor\mcp.json (paris.lan) -- created at correct path, old one at %APPDATA% removed
/var/www/philoenic.com/.cursor/mcp.json -- restored with local python3 command
/var/www/philanthropy-planner.com/.cursor/mcp.json -- restored
/var/www/prospecta/.cursor/mcp.json -- restored
/var/www/prospecta.cc/.cursor/mcp.json -- restored
/APPS/quickstep/.cursor/mcp.json -- restoredLesson learned: For Cursor MCP in SSH remote workspaces, always use project-level configs that run the server locally on the remote machine. Global configs on the client machine are only useful for local (non-SSH) workspaces. The settings UI can be misleading -- "enabled" and "connected" doesn't mean the agent can use the tools.
---
Feature: Zap-Projects -- Chat History & Deep Analysis System
Date: February 2026
Feature: Complete Cursor AI chat history indexing, browsing, and analysis system.
Implementation:
Built scripts/ingest-chat-transcripts.php to scan all Cursor agent-transcripts directories, parse metadata (message counts, tool calls, apps involved, files touched, topics), generate LLM summaries via delphi.lan (qwen3:30b model)
Built scripts/deep-analyse-chat.php for structured JSON analysis of each chat (title, narrative summary, artifacts created, chart work, decisions made, data sources, people mentioned, unfinished work, questions raised)
Built scripts/watch-transcripts.sh using inotifywait to monitor transcript directories for file creates/writes, with 120-second debounce to avoid re-processing active chats
Created systemd service zap-chat-watcher.service for the file watcher
Added cron job (/30 *) for deep analysis as a safety net
Built web UI: chat history browser with FTS5 search, app/project filtering; transcript viewer with rendered HTML, summaries, deep analysis, plan links; AI Q&A endpoint
Built energystats asset indexer and browser
Created shared/libs/LLMClient.php as reusable Ollama client
SQLite database at data/chat-history.sqlite with WAL mode, FTS5 full-text search
23 chats indexed, 348 plan links cross-referenced, all with summaries and deep analysis
Technical Decisions:
Used inotify (inotifywait) rather than polling for file watching -- instant detection, minimal CPU
120-second debounce prevents re-analysis during active chat sessions
Two-step processing: fast metadata ingest first (skip LLM), then deep LLM analysis
FTS5 virtual table kept in sync with triggers for real-time search
Plan decided to migrate from SQLite to PostgreSQL (existing zap DB) for the forthcoming Project Hub, to avoid painful mid-project migration later
Files Created:
scripts/ingest-chat-transcripts.php
scripts/deep-analyse-chat.php
scripts/watch-transcripts.sh
scripts/index-energystats-assets.php
apps/zap-projects/web/chat-history.php
apps/zap-projects/web/chat-transcript.php
apps/zap-projects/web/chat-ask.php
apps/zap-projects/web/energystats-assets.php
shared/libs/LLMClient.php
infra/systemd/orcus_zap-chat-watcher.service---
November 2025
Feature: Home Page Connect Link
Date: November 21, 2025
Feature: Added Connect page to home page Main Pages section.
Implementation:
Added Connect page link card to home page Main Pages section
Positioned as first entry (before Host and Guest interfaces)
Marked with "Primary" badge to indicate recommended entry point
Description explains unified interface that auto-detects role
Notes that hosts can create sessions and manage recordings
Notes that guests can join via invite links
Files Changed:
web/public/index.php - Added Connect page link card to Main Pages section
User Experience:
Connect page now easily discoverable from home page
Users understand it's the unified entry point for both hosts and guests
Clear explanation of auto-detection and role-based functionality
Technical Details:
Connect page card uses same styling as other main page cards
Positioned first in grid to emphasize primary entry point status
Badge styling matches other primary pages (Host, Guest)---
Feature: Chat Participants List
Date: November 20, 2025
Feature: Converted single guest info display to participants list showing all session members.
Implementation:
Replaced chat-sessions-guest-info div with chat-participants-list div
Created renderParticipants() method that displays Host and Guest with their status
Host always shows as "Live" (since host is on the page)
Guest shows "Live" or "Offline" based on connection state
Added truncation support: if more than 10 participants, shows "+X more" message
Participants list is scrollable (max-height: 200px) to handle many participants
Removed "Full chat β" link from tabs area (not functional in this context)
Removed redundant connection status display (now shown in participants list)
Files Changed:
web/public/connect.php - Removed "Full chat β" link, updated CSS for participants list
web/public/chat-sessions.js - Replaced guest info with participants list, added renderParticipants() method
Technical Details:
Participants array structure: { name: string, id: string, status: 'live'|'offline' }
Truncation limit: 10 participants (configurable via maxDisplay variable)
Status colors: Live = #16a34a (green), Offline = #6b7280 (grey)
Each participant item has border-bottom except last item
User Experience:
Clear view of who is in the session
Easy to see connection status at a glance
Ready to expand when more participants are added
Clean, organized list format---
Feature: Sessions & Recordings Panel Tabs
Date: November 20, 2025
Feature: Split Sessions & Recordings panel into separate tabs and added Chat tab.
Implementation:
Replaced single "Sessions & Recordings" header with tab-based interface
Created two tabs: "Sessions" and "Recordings" (Title case, matching sidebar style)
Sessions tab shows list of recent sessions (default active tab)
Recordings tab shows recordings for selected session
Clicking a session automatically switches to Recordings tab and loads recordings
Added "Chat" tab as leftmost tab (order: Chat, Sessions, Recordings)
Chat panel created with blank placeholder ready for message functionality
Tab switching JavaScript handles all three tabs generically
Files Changed:
web/public/connect.php - Updated HTML structure, added CSS for tabs, updated JavaScript for tab switching
Technical Details:
Tabs use same CSS classes and structure as sidebar tabs for consistency
Tab panels show/hide based on active class
JavaScript uses generic tab switching that works for any number of tabs
Selected session ID stored globally for loading recordings when tab switches
User Experience:
Cleaner interface with organized tabs
Easy navigation between Sessions, Recordings, and Chat
Consistent tab styling across application---
Feature: Sidebar Tab Styling Update
Date: November 20, 2025
Feature: Changed sidebar tabs from uppercase to Title case for consistency.
Implementation:
Removed text-transform: uppercase CSS property from .sidebar-tab class
Sidebar tabs now display as "Contacts", "Rooms", "Groups" instead of "CONTACTS", "ROOMS", "GROUPS"
Matches the Title case styling used in Sessions & Recordings tabs
Files Changed:
web/public/includes/sidebar.php - Removed text-transform: uppercase from CSS
User Experience:
Consistent tab styling across all tabs in the application
More readable Title case instead of all uppercase---
Issue: Host Reconnect Button Not Working
Date: November 20, 2025
Problem: Host "Reconnect Remotes" button was not properly cleaning up the guest video element before reinitializing WebRTC, causing reconnection to fail or behave inconsistently.
Root Cause:
cleanupHostWebRTC() function in host-webrtc.js was closing the peer connection but not clearing the guest video element's srcObject
Guest's "Connect" button properly cleared hostVideo.srcObject = null and marked container as empty, but host's cleanup didn't match this behavior
Video element remained with old stream reference, preventing proper reconnection
Solution:
Updated cleanupHostWebRTC() to clear guest video element: guestVideo.srcObject = null
Added container class marking: guestVideoContainer.classList.add('empty')
Matches guest's cleanup behavior for consistent reconnection flow
Files Changed:
web/public/host-webrtc.js - Added guest video element cleanup to cleanupHostWebRTC() function
Technical Details:
Guest video element must be cleared before reinitializing WebRTC to prevent stale stream references
Container marked as empty for UI consistency
Peer connection cleanup already existed, only needed video element cleanup
User Experience:
Host reconnect button now works correctly, matching guest connect button behavior
Reconnection properly resets both peer connection and video elements---
Issue: Connect Page Blank for Anonymous Users
Date: November 20, 2025
Problem: When visiting
/connect without being logged in and without any parameters, users saw a blank page with no guidance on what to do.
Root Cause:
No role auto-detection logic for anonymous users
No helpful message explaining how to use the page
Video UI div hidden by default (requires active class) but wasn't getting it when no role selected
Solution:
Added auto-detection: if logged in with host permissions and no params β auto-default to host
Added auto-detection: if session or invite params present β auto-set to guest
Added helpful welcome message for anonymous users: "Use your Invite URL or log in"
Message shows two clear options with explanations
Video UI div now gets active class when showing welcome message
Files Changed:
web/public/connect.php - Added auto-detection logic, welcome message HTML, conditional active class
Technical Details:
Auto-detection checks: $isLoggedIn && $canHost && !$sessionId && !$inviteSlug β host
Auto-detection checks: $sessionId || $inviteSlug β guest
Welcome message only shown when: !$selectedRole && !$isLoggedIn && !$inviteSlug && !$sessionId
Video UI gets active class when showing message: ($selectedRole || (!$selectedRole && !$isLoggedIn && !$inviteSlug && !$sessionId))
User Experience:
Guests with invite links work automatically (no confusion)
Hosts can just visit /connect without needing ?role=host
Anonymous users see clear guidance on how to use the page---
Feature: Navbar Access Control
Date: November 20, 2025
Feature: Hide Host, Guest, Recordings, and Messages navbar links from non-host users.
Implementation:
Added $canHost permission check to navbar component
Wrapped Host, Guest, Recordings, and Messages links in conditional:
Home, Connect, and Login remain visible to everyone
Admin link still only visible to admins
Files Changed:
web/public/includes/navbar.php - Added $canHost check, wrapped restricted links in conditional
User Experience:
Cleaner navigation for anonymous users (only see Home, Connect, Login)
Non-host users don't see links they can't use
Hosts see full navigation menu---
Issue: Recording Uploads Failing with 500 Errors
Date: November 20, 2025
Problem: Recording uploads were failing with
ERR_INTERNAL_SERVER_ERROR: unexpected response code from hook endpoint (500) from tusd.
Root Cause:
tus-hooks.php had incorrect require_once path: ../../bootstrap.php instead of ../bootstrap.php
PHP script was failing with fatal error before it could log anything
tusd received 500 response from hook endpoint, preventing uploads from completing
Solution:
Corrected bootstrap path from ../../bootstrap.php to ../bootstrap.php
Path now correctly resolves to /var/www/zap/web/bootstrap.php
Files Changed:
web/public/tus-hooks.php - Fixed require_once path for bootstrap.php
Technical Details:
tus-hooks.php is in /var/www/zap/web/public/
bootstrap.php is in /var/www/zap/web/
Correct relative path: ../bootstrap.php (up one level from public/ to web/)
User Experience:
Recordings now successfully upload to server
Post-finish hooks execute correctly for remuxing and metadata insertion---
Issue: Guest Name Persistence and Recording Metadata
Date: November 19, 2025
Problem: Guest name changes were not persisting across page refreshes. When user edited name from "Celine3" to "Celine34", it would revert on refresh. Also, guest name was not included in recording file names or session metadata.
Root Cause:
Priority order was wrong: invite/URL data took precedence over sessionStorage (user edits)
No flag to track whether user had manually edited the name
Guest name not stored in database session metadata
Recording file names used generic "guest" instead of actual guest name
URL parameter would override user's edited name on page refresh
Solution:
Fixed priority order: if zap_guest_name_edited flag is set, use sessionStorage value (ignores URL/invite)
Added zap_guest_name_edited flag in sessionStorage to track manual edits
Created /api/guest-name-update.php endpoint to store guest name in sessions.metadata JSONB column
Updated recorder classes to accept guest name parameter and include it in file names
File names now: ${sessionId}-${guestNameSafe}-master.webm (sanitized for filesystem safety)
Guest name passed to recorder constructors when starting recording
URL updates when user edits, but edited name takes priority on refresh
Files Changed:
web/public/connect.php - Fixed priority logic, added edited flag tracking, added API call to store name
web/public/guest.js - Updated recorder classes to accept guest name, include in file names
web/public/api/guest-name-update.php - New endpoint to store guest name in session metadata
Technical Details:
Priority check: if (guestNameEdited && guestNameFromStorage) { use storage } else { URL > invite > storage }
Guest name sanitized with .replace(/[^a-zA-Z0-9_-]/g, '_') for safe filenames
Session metadata stored as JSONB: { "guest_name": "Celine34" }
Recorder instances created with: new GuestMasterRecorder(stream, sessionId, currentGuestName)
User Experience:
User edits now persist correctly across page refreshes
Recording files are identifiable by guest name
Guest name available in session metadata for future reference---
Feature: Guest Session Header and Status Improvements
Date: November 19, 2025
Feature: Improved guest session header layout and dynamic status messages.
Implementation:
Changed header from "Guest Session [session-id]" to "Session: [session-id]" format
"Session:" is bold, session ID is normal weight (matching user request)
Guest name moved to right side of header with "Your name:" label
Guest name displays as clickable inline text with tooltip
Status message now dynamically reflects actual connection state
Status updates automatically when camera or host connection state changes
Connect button renamed from "Reconnect" and moved to Host Video pane controls
Files Changed:
web/public/connect.php - Restructured header HTML, moved guest name to right, updated status initialization
web/public/guest.js - Added updateConnectionStatus() function, replaced hardcoded status messages
Status Message Format:
"Your camera is off, remote host is not connected."
"Your camera is on, remote host is connected."
Updates in real-time as states change
Technical Details:
Status checks: guestCameraActive (boolean) and hostVideo.srcObject.getTracks().length > 0
Status updates called on: camera toggle, host video stream changes, WebRTC connection state changes, Centrifugo subscription---
Feature: Guest Session Camera and Audio Controls
Date: November 19, 2025
Feature: Added camera and audio controls to guest session page, matching host session functionality.
Implementation:
Added camera on/off button for guest's own camera (left video pane)
Added mute mic button for guest's microphone (disabled until camera active)
Added mute audio button for host's audio feed (disabled until host stream available)
Added disabled camera button for host video (guest cannot control host camera)
All controls use same HTML structure and CSS styling as host session
Buttons properly enable/disable based on stream availability
State management integrated with existing guest.js camera toggle logic
Files Changed:
web/public/connect.php - Added video-box-controls HTML structure to guest session video boxes
web/public/guest.js - Added toggleGuestMicBtn variable, updateGuestMicButtonUI(), toggleGuestMic() functions, integrated mic button state with camera toggle
User Experience:
Guest session now has parity with host session for camera/audio controls
Clear visual feedback for button states (enabled/disabled, muted/unmuted)
Consistent UI/UX across host and guest interfaces
Technical Details:
Guest mic button disabled by default, enabled when camera becomes active
Host audio button enabled when host video stream contains audio tracks
Mic mute state tracked separately from camera state
Button UI updates use same pattern as host session (icon + text updates)---
Feature: Guest Invite Flow and Data Display
Date: November 18, 2025
Feature: Auto-redirect guests with invites directly to session page, display session ID and guest name from invites (even expired/used).
Implementation:
Moved invite lookup before role selection to ensure data is available
Auto-set role to guest when invite parameter is present in URL
Skip role selection page for guests (only show for logged-in hosts)
Added fallback database query to retrieve invite data even if invite is expired/used
Session ID displays inline with "Guest Session" heading when available
Guest name pre-populates in input field from invite data
Added comprehensive console logging for debugging
Improved error handling for invite lookup failures
Files Changed:
web/public/connect.php - Restructured invite lookup flow, added fallback query, improved guest name/session ID display
Issues Fixed:
Guests with invites were seeing role selection page unnecessarily
Session ID not displaying for guests with invites
Guest name not pre-populating from invite data
Expired/used invites losing session_id and guest_name data
Technical Details:
Invite lookup now happens before role check (line 46-99)
Fallback query bypasses findBySlug() validation to get data for display
Role selection only shown if $selectedRole is null AND user is logged in with host permissions
Video UI automatically shows when $selectedRole = 'guest' is set
Session ID and guest name extracted from invite data even if invite is no longer valid---
Issue: Sidebar Tab Spacing and Toggle Button Text
Date: November 16, 2025
Problem: GROUPS tab was too close to the middle content area, and toggle buttons displayed incorrect text (always showing "Close" even when panels were collapsed).
Root Cause:
Sidebar width (520px) was insufficient to provide proper spacing between tabs and middle section
Right padding on .sidebar-tabs container had minimal effect on visual spacing
Toggle button text logic was inverted - showing "Close" when panels were collapsed
CSS visibility rules for button text spans were not properly implemented
Solution:
Increased sidebar width from 520px to 550px in .zap-sidebar CSS rule
Adjusted tab container padding and gap for better visual balance
Fixed toggle button text to show "Open [Noun]" when collapsed, "Close [Noun]" when open
Added proper CSS rules for .header-toggle-text with .collapsed class logic
Verb now precedes noun (e.g., "Open Contacts" instead of "Contacts Open")
Files Changed:
web/public/includes/sidebar.php - Increased sidebar width, adjusted tab padding/gap
web/public/connect.php - Fixed toggle button HTML structure and CSS visibility rules
Technical Details:
Sidebar width is the primary control for spacing between tabs and middle content
Tab container padding has minimal visual effect compared to overall sidebar width
Toggle button state is managed via .collapsed class on button element
Text visibility controlled by CSS rules on .collapsed-text and .open-text spans---
Feature: Camera Controls and UI Improvements
Date: November 16, 2025
Feature: Implemented camera on/off controls under video panes, camera defaults to off, improved button styling and positioning.
Implementation:
Moved camera toggle buttons from main controls to under each video pane
Added mute mic button under host camera (disabled until camera active)
Added mute audio button under guest camera (disabled until host stream available)
Camera button text shows action ("Camera On" when off, "Camera Off" when on)
Removed auto-camera activation on page load
Disabled queueAutoCameraActivation function
Improved disabled button styling for better readability
Moved sidebar/sessions toggle buttons to header row
Simplified "Logged in as" display (name only)
Refactored camera initialization to use DOMContentLoaded
Fixed syntax errors from duplicate code removal
Files Changed:
web/public/connect.php - Updated video box controls, button styling, header layout
web/public/recorder.js - Refactored camera initialization, removed auto-activation, fixed syntax errors
web/public/guest.js - Updated guest camera controls, host audio mute functionality
web/public/includes/sidebar.php - Updated toggle button styling
Issues Fixed:
Camera button not working (fixed initialization timing)
Camera auto-activating on page load (removed auto-activation code)
Disabled buttons not readable (improved contrast and styling)
Syntax error in recorder.js (removed duplicate code)---
Feature: Invite System, Sidebar, and Sessions Panel
Date: November 15, 2025
Feature: Implemented secure invite system, left sidebar for contacts/rooms/groups, and right-hand sessions panel with centered main content layout.
Implementation:
Created database schema for invites, contacts, groups, and sessions tables
Built InvitesRepository, ContactsRepository, GroupsRepository, and SessionsRepository classes
Created API endpoints for invite management, contact/group listing, and session listing
Implemented left sidebar component with collapsible toggle (always visible)
Added right-hand sessions panel with collapsible toggle (always visible)
Integrated invite creation into host UI with contact/group dropdowns
Auto-create session records when invites are created with session_id
Centered main content panel using flexbox with proper spacing
Aligned both toggle buttons at same vertical level (80px from top) to match "Host Session" heading
Session switching from both Session Management section and right panel
Files Changed:
database/migrations/2025-11-15-0003-invites-schema.sql - Invites table schema
database/migrations/2025-11-15-0004-contacts-groups-sessions.sql - Contacts, groups, sessions schemas
database/migrations/2025-11-15-0005-invites-add-contact-group.sql - Add contact_id/group_id to invites
web/public/includes/sidebar.php - Left sidebar component
web/public/connect.php - Updated with sidebar, sessions panel, centered layout
web/public/api/invites-*.php - Invite management endpoints
web/public/api/contacts-list.php, groups-list.php, sessions-list.php - List endpoints
web/src/Invites/, Contacts/, Groups/, Sessions/ - Repository classes
Design Documents:
design/invite-system-design.md - Invite system architecture
design/sidebar-and-sessions-architecture.md - Sidebar and sessions panel design---
Issue: Remote Video Not Showing (Track Stopping Bug)
Date: Early November 2025
Symptoms: Remote video (host/guest) would not display on either side after WebRTC connection established.
Root Cause: Code was calling
stream.getTracks().forEach(track => track.stop()) in
updateGuestVideo() and
updateHostVideo() functions, which stopped the remote media tracks prematurely, causing the connection to fail.
Solution: Removed the track stopping code from both
guest.js and
host-webrtc.js. Remote tracks should not be stopped by the receiving side - they are managed by the sender.
Files Changed:
/var/www/zap/web/public/guest.js - Removed track stopping in updateHostVideo()
/var/www/zap/web/public/host-webrtc.js - Removed track stopping in updateGuestVideo()---
Issue: Session ID Not Persisting
Date: Early November 2025
Symptoms: Host page always showed "Session ID: 123" regardless of URL parameter.
Root Cause: recorder.js was using hardcoded
sessionId = '123' instead of reading from URL.
Solution: Modified
getSessionIdFromURL() in
recorder.js to read
session parameter from URL. Added PHP parsing in
host.php to extract session ID from
$_GET['session'].
Files Changed:
/var/www/zap/web/public/recorder.js - Updated getSessionIdFromURL() to read URL parameter
/var/www/zap/web/public/host.php - Added PHP session ID parsing---
Issue: Browser Cache Preventing JavaScript Updates
Date: Early November 2025
Symptoms: JavaScript fixes not loading in browser even after code changes.
Root Cause: Browser was caching JavaScript files, preventing new code from loading.
Solution: Added cache-busting timestamps (
?v=) to all JavaScript module imports in
host.php and
guest.php.
Files Changed:
/var/www/zap/web/public/host.php - Added cache-busting to host-webrtc.js and recorder.js imports
/var/www/zap/web/public/guest.php - Added cache-busting to guest.js import---
Issue: Network Routing - MacBook Unreachable
Date: Mid November 2025
Symptoms: orcus.lan (192.168.2.32) and Windows 11 (192.168.1.17) could not ping MacBook (192.168.68.54). MacBook could ping both.
Root Cause: MacBook is on a mesh network (192.168.68.0/22) while orcus.lan is on a different subnet (192.168.2.0/24). No routing configured between subnets.
Solution: Established reverse SSH tunnel from MacBook to orcus.lan on port 2222. This allows orcus.lan to connect to MacBook via
ssh -p 2222 juliandarley@localhost. The tunnel is maintained via systemd service on Linux and launchd on macOS.
Files Changed:
/var/www/zap/infra/systemd/macbook-reverse-tunnel.service - Systemd service for tunnel (Linux reference)
/var/www/zap/infra/launchd/com.getzap.reverse-tunnel.plist - Launchd service for tunnel (macOS)
/var/www/zap/docs/macbook-tunnel-setup.md - Documentation for tunnel setup---
Issue: TUS Upload Location Header Malformed
Date: Mid November 2025
Symptoms: TUS upload
Location header was malformed:
https://orcus.getzap.co, zap.orcus.lan, zap.orcus.lan/files/...
Root Cause: Multiple
ProxyPassReverse directives across Apache proxy layers were conflicting and creating duplicate hostnames in the Location header.
Solution:
Removed all ProxyPassReverse directives from zap-subdomain-ssl.conf and zap-tunnel-proxy.conf
Explicitly set X-Forwarded-Host at each proxy layer to ensure correct original hostname
Added regex-based Header edit* Location directive in orcus-getzap-proxy-le-ssl.conf to manually rewrite any remaining malformed Location headers
Files Changed:
/var/www/zap/infra/apache/zap-subdomain-ssl.conf - Removed ProxyPassReverse, added X-Forwarded-Host
/var/www/zap/infra/apache/zap-tunnel-proxy.conf - Removed ProxyPassReverse, added X-Forwarded-Host
/var/www/zap/infra/apache/orcus-getzap-proxy-le-ssl.conf - Added Header edit* Location directive---
Issue: Windows 11 Camera "In Use by Another Application"
Date: Late November 2025
Symptoms: Chrome on Windows 11 would show "Camera/microphone is in use by another application" error, even after closing all other apps. Camera worked in Edge and Chrome Incognito, but not in normal Chrome.
Root Cause: Chrome Sync was re-enabling extensions from Google account even when disabled locally. One or more of the 70+ synced extensions was holding the camera handle.
Solution: Disabled extension syncing in Chrome preferences (
extensions and
extension_settings sync settings). This prevents Chrome Sync from re-enabling the problematic extension(s).
Additional Fixes:
Added retry mechanism with exponential backoff (max 3 retries) for getUserMedia calls
Added device preference for "HD Pro Webcam C920" and "Line In (Saffire 6 USB)"
Created diagnostic PowerShell scripts for camera troubleshooting
Files Changed:
/var/www/zap/web/public/recorder.js - Added retry mechanism and device preference
/var/www/zap/web/public/guest.js - Added retry mechanism and device preference
Chrome preferences file (via PowerShell) - Disabled extension sync---
Issue: Guest Recording Duration Only 0.4-2 Seconds
Date: Late November 2025
Symptoms: Guest recordings on MacBook were only 0.4-2 seconds long, even when recording for 20+ seconds.
Root Cause: pollRecordingState() function was checking host recording state every 2 seconds. When host was NOT recording but guest WAS recording, it automatically called
stopRecording() to sync with host. This caused guest recording to stop after ~750ms.
Solution: Added
isManualTestRecording flag that:
Gets set to true when Test Record button is clicked
Tells pollRecordingState() to skip auto-stop logic during manual test recording
Gets cleared when recording stops
Files Changed:
/var/www/zap/web/public/guest.js - Added isManualTestRecording flag and logic in pollRecordingState() and Test Record button handler---
Issue: Fallback Recording Never Uploading
Date: Late November 2025
Symptoms: Fallback recordings were being created and concatenated successfully, but never uploaded to server.
Root Cause: Master upload was starting first and setting
uploadQueue.uploading = true. When fallback file was queued for immediate upload,
startUploadQueue() would see
uploadQueue.uploading = true and skip processing the fallback queue.
Solution: Added separate
uploadQueue.fallbackUploading flag so fallback uploads can run in parallel with master uploads.
startUploadQueue() now only checks
uploadQueue.fallbackUploading instead of the shared
uploadQueue.uploading flag.
Files Changed:
/var/www/zap/web/public/guest.js - Added uploadQueue.fallbackUploading flag and updated startUploadQueue() logic---
Issue: Cannot See MacBook Chrome Console Logs
Date: Late November 2025
Symptoms: Could not access MacBook Chrome console logs for debugging, making it impossible to diagnose issues remotely.
Root Cause: Browser extension was connected to Windows Chrome, not MacBook Chrome. SSH access to MacBook didn't provide browser console access.
Solution: Implemented automatic console log capture in
guest.php that:
Intercepts all console methods (log, error, warn, info, debug)
Queues logs and sends them to /api/debug-log.php every 2 seconds or when queue reaches 20
Uses sendBeacon for reliable delivery on page unload
Stores logs in /tmp/zap-guest-debug.log which can be read via SSH
Files Changed:
/var/www/zap/web/public/guest.php - Added automatic console log capture script
/var/www/zap/web/public/api/debug-log.php - Created endpoint to receive and store console logs---
Issue: Remote Cameras Working But Playback/Upload Issues (WIP)
Date: Late November 2025
Status: π‘ Work In Progress
Symptoms:
β
Remote cameras working (host and guest can see each other via WebRTC)
β Uploaded host file won't play (needs investigation)
β Guest machine uploaded nothing during multi-participant session (needs investigation)
Root Cause: Under investigation
Solution: Pending investigation
Files Changed:
None yet - investigation in progress---
Issue: SSE (Server-Sent Events) Connection Instability
Date: November 10, 2025
Symptoms: Guest's EventSource connection would open briefly, then revert to CONNECTING or CLOSED state, preventing commands from being received. PHP warnings "Undefined array key 'type'" were also observed.
Root Cause:
SSE connection was not properly established - PHP was not sending initial data event
PHP-FPM/Apache environment was closing connections prematurely
Missing null coalescing for array keys causing PHP warnings
Solution:
Added header('Content-Type: text/event-stream; charset=utf-8') and proper SSE headers
Sent initial connection_established event to trigger EventSource.onopen
Removed fastcgi_finish_request() which was closing the connection
Added set_time_limit(310) and ignore_user_abort(false) for longer connections
Changed sleep(1) to usleep(100000) for faster responsiveness
Added null coalescing for $input['type'] to prevent PHP warnings
Added connection_aborted() checks to detect client disconnects
Files Changed:
/var/www/zap/web/public/api/session-signal.php - Improved SSE headers and connection handling
/var/www/zap/web/public/guest.js - Updated SSE error handling and reconnection logic---
Issue: Apache WebSocket Proxy Rejecting Upgrade Response
Date: November 10, 2025
Symptoms: PHP WebSocket server handshake succeeds (logs show "Handshake sent successfully"), but browser immediately disconnects with "Unexpected response code: 502/503". Apache error logs show "AH00898: Unexpected Upgrade: websocket (expecting n/a)".
Root Cause: Apache's
ProxyPass with
http:// protocol was treating WebSocket upgrade as HTTP, not WebSocket. Apache expects
ws:// protocol for WebSocket connections, but even with
ws:// in ProxyPass, Apache was still rejecting the Upgrade response.
Solution Attempts:
Enabled proxy_wstunnel module in Apache
Changed ProxyPass /ws http://127.0.0.1:8080/ to ProxyPass /ws ws://127.0.0.1:8080/
Added RewriteCond %{HTTP:Upgrade} websocket [NC] and RewriteCond %{HTTP:Connection} upgrade [NC] to RewriteRule
Moved WebSocket ProxyPass directives before PHP handler to prevent PHP-FPM from intercepting
Improved PHP WebSocket server handshake logic (non-blocking sockets, better error handling)
Result: Handshake succeeds on server side, but Apache still rejects Upgrade response.
Recommended Solution: Use Centrifugo (Go-based WebSocket appliance) instead of PHP WebSocket server. Centrifugo runs as standalone service, no Apache proxy issues, works perfectly with PHP via HTTP API.
Files Changed:
/var/www/zap/scripts/websocket-server.php - PHP WebSocket server implementation
/var/www/zap/infra/apache/zap-subdomain-ssl.conf - WebSocket proxy configuration (attempted)
/var/www/zap/web/public/test-ws-host.html - WebSocket test page for host
/var/www/zap/web/public/test-ws-guest.html - WebSocket test page for guest
/var/www/zap/design/zap_signalling_architecture.md - Architecture document recommending Centrifugo---
Issue: Centrifugo v6.5.0 Configuration Format Changed
Date: November 10, 2025
Symptoms: Centrifugo service failing to start with config errors: "error unmarshalling config: decoding failed due to the following error(s): 'admin' expected a map or struct, got \"bool\"". Service would start but not listen on port 25001.
Root Cause: Centrifugo v6.5.0 uses a different configuration format than earlier versions. The design document (
zap_signalling_and_centrifugo.md) was written for an older version. New format requires nested objects instead of flat keys:
port β http_server.port
address β http_server.address
admin: true β admin.enabled: true
token_hmac_secret_key β token_auth.hmac_secret_key
Solution: Updated config file to use v6.5.0 format with nested objects. Centrifugo now starts correctly and listens on port 25001.
Files Changed:
/var/www/zap/infra/centrifugo/config.json - Updated to v6.5.0 format with nested objects
/var/www/zap/infra/systemd/orcus_centrifugo.service - Systemd service file
/var/www/zap/infra/apache/zap-subdomain-ssl.conf - Apache proxy configuration---
Issue: Centrifugo Installation and Setup
Date: November 10, 2025
Status: β
Resolved
Summary: Installed and configured Centrifugo v6.5.0 for real-time messaging and signalling.
Implementation:
Downloaded and installed Centrifugo v6.5.0 binary to /usr/local/bin/centrifugo
Created config file at /etc/centrifugo/config.json with correct v6.5.0 format
Created systemd service orcus_centrifugo.service (following zap project pattern with orcus_ prefix)
Configured Apache proxy at /centrifugo/ path to forward to http://127.0.0.1:25001/
Created test pages (test-centrifugo-host.html, test-centrifugo-guest.html) for connection testing
Verified Centrifugo is running and responding via Apache proxy
Configuration:
Port: 25001 (localhost only)
Namespaces: signal (no history, server-publish only) and chat (1-day history, client-publish allowed)
Admin interface: Enabled (insecure mode for testing)
API key and JWT secret: Configured (to be moved to environment variables)
Files Created:
/var/www/zap/infra/centrifugo/config.json - Centrifugo configuration
/var/www/zap/infra/systemd/orcus_centrifugo.service - Systemd service file
/var/www/zap/web/public/test-centrifugo-host.html - Host test page
/var/www/zap/web/public/test-centrifugo-guest.html - Guest test page
Files Modified:
/var/www/zap/infra/apache/zap-subdomain-ssl.conf - Added Centrifugo proxy configuration
Next Steps:
Create PHP CentrifugoClient class for publishing messages via HTTP API
Create JWT generator class for browser client authentication
Integrate Centrifugo into host/guest recording signalling
Move API key and JWT secret to environment variables---
Issue: Centrifugo WebSocket 400 via Apache proxy
Date: 10 November 2025 (continuing)
Symptoms: Browser and CLI clients (
websocat wss://orcus.getzap.co/centrifugo/connection/websocket) receive
400 Bad Request and Centrifugo logs
websocket: the client is not using the websocket protocol: 'upgrade' token not found in 'Connection' header whenever the request flows through Apache. Direct connections to Centrifugo on
ws://127.0.0.1:25001/connection/websocket succeed instantly.
Root Cause (WIP): Apache is still stripping or overwriting the
Connection: Upgrade /
Upgrade: websocket headers despite enabling
mod_proxy_wstunnel and experimenting with
ProxyPass,
RewriteRule, and explicit
RequestHeader directives. Need to determine exactly where the header is lost and adjust the vhost accordingly.
Investigation To Date:
Verified Centrifugo service works standalone using websocat ws://127.0.0.1:25001/connection/websocket
Tried multiple Apache configurations (direct ProxyPass, , RewriteRule, RequestHeader set, forcing Protocols http/1.1)
Captured Centrifugo logs and repeatedly saw missing Upgrade headers when routed via Apache
Collected tcpdump traces on loopback to inspect forwarded traffic (headers still absent)
Next Steps:
Continue refining Apache vhost (potentially use VirtualHost-scoped SetEnvIf Upgrade websocket patterns)
Consider temporary bypass or dedicated upstream proxy until Apache forwards headers cleanly---
Issue: Centrifugo integration into host/guest signalling (Phase 2 continuing)
Date: November 10, 2025
Status: π‘ Work In Progress
Summary: Completed the PHP + browser wiring so host and guest pages connect to Centrifugo via JWTs and exchange recording commands in real time. Still need to migrate WebRTC offer/answer/ICE traffic and retire the legacy
session-signal.php endpoints after full regression tests.
Changes:
web/public/recorder.js: Added Centrifugo client bootstrap, token caching, subscription resilience, and record:start / record:stop publishing via /api/centrifugo-publish.php
web/public/guest.js: Replaced custom /ws listener with Centrifugo subscription, status publishing, and start/stop handling
web/public/api/centrifugo-publish.php: Accepted {channel,data} payloads for generic publishes and improved error handling
web/src/Realtime/CentrifugoClient.php: Allowed extra fields when publishing start/stop/chat events so timing metadata propagates
web/public/host.php & web/public/guest.php: Added local centrifuge.min.js include to guarantee client availability during testingNext Steps:
Move WebRTC offer/answer/ICE signalling onto Centrifugo so /api/session-signal.php can be deprecated
Exercise full hostβguest recording flow over Centrifugo and confirm uploads stay stable
Harden secrets management (ensure systemd env vars override placeholders everywhere)---
Issue: Move WebRTC signalling onto Centrifugo
Date: November 10, 2025
Status: β
Completed
Summary: Replaced the polling/SSE bridge with Centrifugo for SDP and ICE exchange. Signal namespace history (20 messages / 2 minutes) ensures late subscribers can replay the most recent offer/answer. Host/guest pages now publish
webrtc:offer,
webrtc:answer, and
webrtc:ice events;
session-signal.php remains only as a fallback pending regression tests.
Changes:
infra/centrifugo/config.json: enabled history for the signal namespace (size 20, TTL 2 minutes)
web/public/recorder.js: tracked offsets, replayed history, and routed guest WebRTC publications into host-webrtc.js
web/public/host-webrtc.js: removed polling, exposed processGuestAnswer/processGuestIceCandidate, and published offer/ICE via Centrifugo
web/public/guest.js: consumed Centrifugo history, ignored self-published events, and published answer/ICE via publishGuestSignal
web/public/api/centrifugo-publish.php & web/src/Realtime/CentrifugoClient.php: accepted generic payloads and allowed extra metadata on publish helpers
Documentation updates (README.md, CHANGELOG.md) describing the migrationNext Steps:
Perform full Win11 β MacBook recording regression now that WebRTC signalling is pub/sub based
Remove /api/session-signal.php and associated storage once field tests confirm stability
Automate Centrifugo service reload/deploy so config history changes propagate safely---
Issue: Fallback Recordings Succeed but Host Audio + Master Streams Missing
Date: November 11, 2025
Status: π‘ Work In Progress
Symptoms:
β
Fallback recordings generated for both host (Windows 11) and guest (MacBook)
β Host fallback WebM plays with no audio track
β No master recordings produced for either participant (expected *-master.webm absent)
Investigation To Date:
Confirmed MediaRecorder fallback path invoked on both sides via debug logs
Verified TUS uploads completed for fallback files only; master queue never started
Observed new Centrifugo-driven toggle flow calling cleanupHostWebRTC() when camera hidden; need to confirm this does not drop master recorder tracks
Next Steps:
Inspect master recorder pipeline in recorder.js to ensure tracks rebind after camera toggle
Capture full debug log timeline for record:start / record:stop to confirm publish payloads include mode: master
Compare guest vs host MediaRecorder constraints, focusing on Windows audio device selection and track cloning order---
Issue: Manual reconnect flow required full page reload
Date: November 11, 2025
Status: β
Resolved
Symptoms: Host/guest needed to refresh the entire page to recover WebRTC or Centrifugo disconnects after network hiccups.
Root Cause: No UI hook existed to reset the Centrifugo subscription + RTCPeerConnection, so stale state persisted until a full reload.
Solution:
Added π Reconnect buttons on host (reconnect-remotes) and guest (reconnect-remotes) pages.
Buttons call new helpers (attemptHostReconnect, attemptGuestReconnection) that reset Centrifugo token/subscription, reinitialise WebRTC, and optionally notify the remote participant via reconnect:request.
reconnect:request publications now trigger the opposite side to retry automatically; host logs reconnect attempts via HOST_RECONNECT_*, guest uses debugLog.
Enabled allow_history_for_client in signal namespace to prevent Centrifugo history permission errors during recovery.
Files Changed:
web/public/host.php
web/public/guest.php
web/public/recorder.js
web/public/guest.js
infra/centrifugo/config.json---
Issue: mkcert CA warning showing on public domain
Date: November 12, 2025
Status: β
Resolved
Symptoms: mkcert CA installation warning appeared on
orcus.getzap.co (public domain) even though it uses Let's Encrypt and doesn't need client cert installation.
Root Cause: Architecture misunderstanding -
orcus.getzap.co is proxied through du2 (Let's Encrypt) β SSH tunnel β orcus.lan β zap.orcus.lan vhost. The tunnel proxy sets
Host: zap.orcus.lan for vhost matching, so PHP code checking
HTTP_HOST saw
zap.orcus.lan even when accessed via public domain.
Solution: Updated detection logic to check
X-Forwarded-Host header (set by du2 proxy, preserved by tunnel proxy) to detect public domain access. Warning now only shows for direct access to
zap.orcus.lan (local domain, mkcert), not for
orcus.getzap.co (public domain, Let's Encrypt).
Files Changed:
web/public/index.php
README.md (added detailed architecture documentation)---
Issue: No common navigation across main pages
Date: November 12, 2025
Status: β
Resolved
Symptoms: Main pages (home, host, guest, recordings) had no common navigation, making it difficult to move between pages.
Root Cause: Each page was standalone with no shared navigation component.
Solution: Created reusable navbar component (
includes/navbar.php) with sticky positioning, responsive design (hamburger menu on small devices, full menu on larger), and active page highlighting. Integrated into all main pages via PHP include. Also created comprehensive home page with organized sections for main pages, test pages, and system status.
Files Changed:
web/public/includes/navbar.php (new)
web/public/index.php (home page redesign)
web/public/host.php
web/public/guest.php
web/public/recordings.php---
Issue: Messaging data lacked durable server-side store
Date: November 14, 2025
Status: β
Resolved
Symptoms: Chat prototypes published messages directly to Centrifugo with no persistence, so reconnects lost context and nothing enforced idempotency or read receipts.
Solution: Provisioned PostgreSQL
zap database (role
zap_user) plus migration
database/migrations/2025-11-12-0001-chat-schema.sql (rooms, members, messages, receipts, attachments). Added PDO connection helper and
ChatRepository for room bootstrap, history, and read receipts.
Files Changed:
database/migrations/2025-11-12-0001-chat-schema.sql
web/src/Database/Connection.php
web/src/Chat/ChatRepository.php
web/bootstrap.php---
Issue: Chat API needed durable publish path
Date: November 14, 2025
Status: β
Resolved
Symptoms: Browsers could not send/receive chat data with persistence; no endpoints existed to bridge Centrifugo with the new Postgres schema.
Solution: Implemented REST endpoints (
chat-send,
chat-history,
chat-read,
chat-rooms) that validate input, enforce idempotent UUIDs, and publish canonical payloads via
CentrifugoClient. Updated README + design docs with migration + env instructions.
Files Changed:
web/public/api/chat-send.php
web/public/api/chat-history.php
web/public/api/chat-read.php
web/public/api/chat-rooms.php
design/zap_messaging_foundation.md
README.md
CHANGELOG.md
web/public/messages.php
web/public/messages.js---
Lessons Learned
Never stop remote media tracks - Remote tracks are managed by the sender, not the receiver
Always use cache-busting for JavaScript files during development
Reverse SSH tunnels are essential for debugging remote devices on different subnets
Chrome Sync can cause issues - Disable extension sync if extensions are causing problems
Polling can interfere with manual operations - Use flags to prevent polling from interfering with manual actions
Parallel uploads need separate flags - Don't use a shared flag for parallel operations
Console log capture is essential for remote debugging - Implement early in development---
Future Improvements
Consider using WebRTC data channels for state synchronization instead of polling
Implement proper error recovery for failed uploads
Add retry logic for failed uploads
Consider using IndexedDB instead of OPFS for better browser support
Add metrics/monitoring for recording quality and upload success rates---
Issue: Messaging API 500 Errors - Missing Composer Dependencies
Date: November 15, 2025
Symptoms: /api/chat-rooms.php and other messaging endpoints returning 500 errors with "Class 'Dotenv\Dotenv' not found"
Root Cause:
Composer dependencies were not installed because php-intl extension was missing (required by Composer's Symfony components)
.env file had permissions 600 (readable only by owner jd), but PHP-FPM runs as www-data, so it couldn't read database credentials
Solution:
Installed php8.3-intl extension via apt-get
Ran composer install in web/ directory to install dependencies
Changed .env file permissions to 640 and set group to www-data: chmod 640 env/.env && chgrp www-data env/.env
Regenerated Composer autoloader: composer dump-autoload
Restarted PHP-FPM to load new extension
Files Changed:
/var/www/zap/web/composer.lock - Generated by Composer install
/var/www/zap/env/.env - Permissions changed (not in git)---
Issue: Duplicate Messages in Messaging UI
Date: November 15, 2025
Symptoms: When sending a message, the originator sees it twice in the chat interface
Root Cause: Message was being added to the UI twice:
Once from the API response in sendMessage() function
Once from Centrifugo subscription handler when the message was published back
The deduplication check only checked
serverMessageId, but there was a race condition where the message could arrive from Centrifugo before the API response was processed, or vice versa
Solution: Added deduplication checks in both places, checking both
serverMessageId and
clientMessageId:
In sendMessage(): Check for duplicates before adding message from API response
In Centrifugo subscription handler: Check for duplicates by both IDs before adding
Files Changed:
/var/www/zap/web/public/messages.js - Added deduplication logic in sendMessage() and subscription handler---
Issue: Messaging UI Not Restoring State on Page Refresh
Date: November 15, 2025
Symptoms: After refreshing the page, the left panel showed the last active room, but the right panel showed "No room selected" until manually clicking the room again
Root Cause: Rooms were being loaded from localStorage and rendered, but no automatic selection of the most recently active room was happening on page load
Solution: Added auto-selection logic on page load that finds the most recently active room (by
lastActive timestamp) and automatically calls
selectRoom() to restore the UI state, load history, and connect to Centrifugo
Files Changed:
/var/www/zap/web/public/messages.js - Added auto-selection logic after loadRooms() and renderRooms()---
Issue: Unnecessary use Throwable; Statements in API Files
Date: November 15, 2025
Symptoms: PHP warnings: "The use statement with non-compound name 'Throwable' has no effect"
Root Cause: Throwable is a built-in PHP interface, not a class that needs to be imported
Solution: Removed
use Throwable; statements from all API files
Files Changed:
/var/www/zap/web/public/api/chat-rooms.php
/var/www/zap/web/public/api/chat-send.php
/var/www/zap/web/public/api/chat-read.php
/var/www/zap/web/public/api/chat-history.php---
Feature: Authentication and Authorization Foundation (WIP)
Date: November 15, 2025
Implementation: Built complete authentication system foundation for user login and role-based permissions
Components Created:
PostgreSQL migrations for users and user_permissions tables
Session helper class for PHP session management (login, logout, permission checks)
UsersRepository for user lookup and permission queries
API endpoints: /api/auth-login.php, /api/auth-logout.php, /api/auth-check.php
Test user creation script (scripts/create-test-user.php)
Browser test page (/test-auth.html)
Database Schema:
users table: email (unique), password_hash, display_name, timestamps
user_permissions table: can_host, can_manage_recordings, granted_by, notes
Test User: test@example.com /
test123 (can_host: true, can_manage_recordings: true)
Next Steps: Create
/login.php page and
/connect page with authorization checks
Files Created:
/var/www/zap/database/migrations/2025-11-15-0002-users-auth-schema.sql
/var/www/zap/web/src/Auth/Session.php
/var/www/zap/web/src/Auth/UsersRepository.php
/var/www/zap/web/public/api/auth-login.php
/var/www/zap/web/public/api/auth-logout.php
/var/www/zap/web/public/api/auth-check.php
/var/www/zap/scripts/create-test-user.php
/var/www/zap/web/public/test-auth.html
/var/www/zap/docs/testing-auth-and-migration.md---
Feature: Dual-Write Migration SQLite β PostgreSQL (WIP)
Date: November 15, 2025
Implementation: Started migration from SQLite to PostgreSQL for recordings metadata
Strategy: Dual-write approach - write to both databases simultaneously during transition period
Components Created:
PostgreSQL migration for recordings table (2025-11-15-0001-recordings-schema.sql)
RecordingsRepository class for PostgreSQL operations
Updated tus-hooks.php to write to both SQLite and PostgreSQL
Migration Phases:
β
Phase 1: Dual-write (both databases) - ACTIVE
β³ Phase 2: Migrate existing SQLite data to PostgreSQL (script needed)
β³ Phase 3: Switch reads from SQLite to PostgreSQL
β³ Phase 4: Remove SQLite code
Benefits: Single database, better concurrency, easier backups, can join recordings with chat rooms by session_id
Safety Features:
Idempotent writes (won't duplicate if hook called twice)
Graceful degradation: PostgreSQL errors don't break SQLite writes
Both databases remain functional during transition
Files Created/Modified:
/var/www/zap/database/migrations/2025-11-15-0001-recordings-schema.sql
/var/www/zap/web/src/Recordings/RecordingsRepository.php
/var/www/zap/web/public/tus-hooks.php - Added PostgreSQL dual-write---
Feature: Admin Panel and Password Management System
Date: November 15, 2025
Status: β
Complete
Description: Implemented comprehensive admin panel, user registration, password management with one-time codes, and improved user experience with navbar dropdown menu.
Features Implemented:
Admin panel (/admin.php) for user management - create users, assign permissions, view all users
User registration (/register.php) with auto-login
Password management: temporary passwords, one-time login codes (12-hour validity), password reset
Account page (/account.php) for user profile and password management
Connect page improvements: auto-role selection, guest name support, user info display
Navbar user dropdown with account, change role, and logout options
Email helper class (logs to file, ready for SMTP integration)Database Changes:
Added password management columns to users table: password_change_required, temporary_password_hash, one_time_code, one_time_code_expires_at, is_admin
Migration script: database/migrations/2025-01-20-0001-password-management.sqlAPI Endpoints Created:
/api/admin-users.php - Admin user management (list, create, update permissions)
/api/auth-register.php - User registration
/api/auth-reset-password.php - Password reset with one-time code generation
/api/auth-set-password.php - Password changeRepository Updates:
UsersRepository - Added methods for password management, one-time codes, admin checks, user creation with temp passwords
Session - Added admin and password change trackingFiles Created:
web/public/admin.php - Admin panel UI
web/public/account.php - Account settings page
web/public/register.php - Registration page
web/public/logout.php - Logout page
web/public/api/admin-users.php - Admin API
web/public/api/auth-register.php - Registration API
web/public/api/auth-reset-password.php - Password reset API
web/public/api/auth-set-password.php - Password change API
web/src/Utils/EmailHelper.php - Email helper class
database/migrations/2025-01-20-0001-password-management.sql - Password management schemaFiles Modified:
web/public/login.php - Added one-time code support, password change modal, forgot password
web/public/connect.php - Added role auto-selection, guest name support, user info display
web/public/includes/navbar.php - Added user dropdown menu with account, change role, logout
web/public/api/auth-login.php - Added one-time code and password change handling
web/src/Auth/Session.php - Added admin and password change tracking
web/src/Auth/UsersRepository.php - Added password management methodsUser Experience Improvements:
Replaced "Change Role" button with logged-in user display text
User icon in navbar with dropdown menu
Auto-role selection (host if logged in with permissions, guest otherwise)
Guest name support via URL parameter and sessionStorage
Admin panel with temporary password display (show/hide, copy button)