Sanyam0385 commited on
Commit
49d46a6
Β·
1 Parent(s): 0f55048

Fix own-UI click leakage, S/E firing on the report page, add X kill switch

Browse files

Mouse tracker now excludes its own panel/pill/canvas from every
measurement (trail, heatmap, dwell, clicks) instead of logging interactions
with its own controls as website activity. S/E are now gated to actual
website pages (landing and report pages are marked non-trackable, with a
Python-side URL check as a backstop). X is a new global hotkey that kills
the process outright via Api.quit_app().

Files changed (2) hide show
  1. analysis.py +1 -0
  2. browser_session.py +76 -30
analysis.py CHANGED
@@ -273,6 +273,7 @@ _TEMPLATE = r"""<!DOCTYPE html>
273
  <html>
274
  <head>
275
  <meta charset="utf-8">
 
276
  <title>InsightUX Session Report</title>
277
  <style>
278
  :root {
 
273
  <html>
274
  <head>
275
  <meta charset="utf-8">
276
+ <meta name="insightux-report" content="true">
277
  <title>InsightUX Session Report</title>
278
  <style>
279
  :root {
browser_session.py CHANGED
@@ -225,34 +225,40 @@ class OneEuroFilter:
225
  CONTROL_JS = r"""
226
  (function(){
227
  if (window.__insightuxControl) { return; }
228
- // The local landing page is already fully InsightUX-branded β€” don't
229
- // stack a second banner on top of it.
230
- if (document.querySelector('meta[name="insightux-landing"]')) { return; }
231
  window.__insightuxControl = true;
232
 
233
- const banner = document.createElement('div');
234
- banner.id = '__insightux_pill';
235
- banner.style.cssText = `
236
- position:fixed; top:0; left:0; right:0; z-index:2147483647;
237
- display:flex; align-items:center; justify-content:space-between;
238
- padding:7px 16px; font:12px -apple-system,Arial;
239
- background:linear-gradient(90deg, rgba(20,18,26,0.95), rgba(30,20,40,0.95));
240
- border-bottom:2px solid; border-image:linear-gradient(90deg,#7B2FBE,#FF2DF0) 1;
241
- box-shadow:0 3px 14px rgba(0,0,0,0.35); pointer-events:none;
242
- `;
243
- banner.innerHTML = `
244
- <span style="display:flex;align-items:center;gap:7px;">
245
- <span style="width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#7B2FBE,#FF2DF0);display:inline-block;"></span>
246
- <b style="color:#f0ecf7;letter-spacing:0.02em;">InsightUX</b>
247
- <span style="color:#7a7288;">Eye + Mouse Research Browser</span>
248
- </span>
249
- <span id="__insightux_status" style="color:#c9a6f5;">Press S to start tracking &middot; E to stop &middot; H heatmap &middot; M mouse panel</span>
250
- `;
251
- document.documentElement.appendChild(banner);
252
-
253
- setInterval(function(){
254
- if (!document.documentElement.contains(banner)) document.documentElement.appendChild(banner);
255
- }, 1000);
 
 
 
 
 
 
 
 
 
256
 
257
  const statusEl = () => document.getElementById('__insightux_status');
258
  window.insightuxSetStatus = function(text){
@@ -270,6 +276,15 @@ CONTROL_JS = r"""
270
  document.addEventListener('keydown', function(e){
271
  const typingBlocked = isTypingTarget(e.target) || isTypingTarget(document.activeElement);
272
  const apiReady = !!(window.pywebview && window.pywebview.api);
 
 
 
 
 
 
 
 
 
273
  if (e.key === 's' || e.key === 'S' || e.key === 'e' || e.key === 'E') {
274
  console.log('[insightux] key=' + e.key, 'typingBlocked=' + typingBlocked, 'apiReady=' + apiReady, 'focusedEl=' + (document.activeElement && document.activeElement.tagName));
275
  }
@@ -368,9 +383,9 @@ TRACKING_JS = r"""
368
  function consider(el){
369
  if (full) return;
370
  // Never track InsightUX's own injected UI (the "press S/E" pill,
371
- // the gaze-dot canvas) as if it were page content.
372
- if (el.id === '__insightux_pill' || el.id === '__insightux_canvas') return;
373
- if (el.closest && el.closest('#__insightux_pill, #__insightux_canvas')) return;
374
  const tag = el.tagName.toLowerCase();
375
  const explicit = !!(el.dataset && el.dataset.aoi);
376
  if (!explicit){
@@ -646,8 +661,18 @@ MOUSE_JS = r"""
646
  updatePanel();
647
  }, 2000);
648
 
 
 
 
 
 
 
 
 
 
649
  // ---- mouse position + hover tracking ----
650
  function updateMousePos(e){
 
651
  lastClientX = e.clientX; lastClientY = e.clientY;
652
  if (lastPageX === 0) lastPageX = e.pageX;
653
  if (lastPageY === 0) lastPageY = e.pageY;
@@ -665,6 +690,7 @@ MOUSE_JS = r"""
665
  document.addEventListener('click', function(e){
666
  if (!isTracking) return;
667
  if (e.target === canvas) return;
 
668
  if (!isInteractable(e.target)) return;
669
 
670
  const x = e.pageX, y = e.pageY;
@@ -962,9 +988,15 @@ MOUSE_JS = r"""
962
 
963
 
964
  # =============================================================================
965
- # API exposed to JS β€” start/stop are the only two entry points
966
  # =============================================================================
967
 
 
 
 
 
 
 
968
  class Api:
969
  def __init__(self):
970
  self.window = None
@@ -979,6 +1011,14 @@ class Api:
979
  if self.tracking:
980
  print("[browser_session] already tracking, ignoring")
981
  return False
 
 
 
 
 
 
 
 
982
  if not os.path.exists(CALIBRATION_PATH):
983
  print(f"[browser_session] no {CALIBRATION_PATH} found β€” run calibrate.py first")
984
  self._set_status("No calibration.pkl found β€” run calibrate.py first.")
@@ -1021,6 +1061,12 @@ class Api:
1021
  self.mouse_close_timer.start()
1022
  return True
1023
 
 
 
 
 
 
 
1024
  def _inject_tracking_overlay(self):
1025
  try:
1026
  self.window.evaluate_js(TRACKING_JS)
 
225
  CONTROL_JS = r"""
226
  (function(){
227
  if (window.__insightuxControl) { return; }
 
 
 
228
  window.__insightuxControl = true;
229
 
230
+ // S/E (start/stop tracking) only make sense on an actual website β€” not on
231
+ // the InsightUX landing/search page and not on a generated insights
232
+ // report page. X (quit the app) always works, on every page.
233
+ const isLanding = !!document.querySelector('meta[name="insightux-landing"]');
234
+ const isReport = !!document.querySelector('meta[name="insightux-report"]');
235
+ const isTrackable = !isLanding && !isReport;
236
+
237
+ if (isTrackable) {
238
+ const banner = document.createElement('div');
239
+ banner.id = '__insightux_pill';
240
+ banner.style.cssText = `
241
+ position:fixed; top:0; left:0; right:0; z-index:2147483647;
242
+ display:flex; align-items:center; justify-content:space-between;
243
+ padding:7px 16px; font:12px -apple-system,Arial;
244
+ background:linear-gradient(90deg, rgba(20,18,26,0.95), rgba(30,20,40,0.95));
245
+ border-bottom:2px solid; border-image:linear-gradient(90deg,#7B2FBE,#FF2DF0) 1;
246
+ box-shadow:0 3px 14px rgba(0,0,0,0.35); pointer-events:none;
247
+ `;
248
+ banner.innerHTML = `
249
+ <span style="display:flex;align-items:center;gap:7px;">
250
+ <span style="width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#7B2FBE,#FF2DF0);display:inline-block;"></span>
251
+ <b style="color:#f0ecf7;letter-spacing:0.02em;">InsightUX</b>
252
+ <span style="color:#7a7288;">Eye + Mouse Research Browser</span>
253
+ </span>
254
+ <span id="__insightux_status" style="color:#c9a6f5;">Press S to start tracking &middot; E to stop &middot; H heatmap &middot; M mouse panel &middot; X quit</span>
255
+ `;
256
+ document.documentElement.appendChild(banner);
257
+
258
+ setInterval(function(){
259
+ if (!document.documentElement.contains(banner)) document.documentElement.appendChild(banner);
260
+ }, 1000);
261
+ }
262
 
263
  const statusEl = () => document.getElementById('__insightux_status');
264
  window.insightuxSetStatus = function(text){
 
276
  document.addEventListener('keydown', function(e){
277
  const typingBlocked = isTypingTarget(e.target) || isTypingTarget(document.activeElement);
278
  const apiReady = !!(window.pywebview && window.pywebview.api);
279
+
280
+ if (e.key === 'x' || e.key === 'X') {
281
+ if (typingBlocked || !apiReady) return;
282
+ window.pywebview.api.quit_app();
283
+ return;
284
+ }
285
+
286
+ if (!isTrackable) return; // S/E do nothing on the landing page or a report page
287
+
288
  if (e.key === 's' || e.key === 'S' || e.key === 'e' || e.key === 'E') {
289
  console.log('[insightux] key=' + e.key, 'typingBlocked=' + typingBlocked, 'apiReady=' + apiReady, 'focusedEl=' + (document.activeElement && document.activeElement.tagName));
290
  }
 
383
  function consider(el){
384
  if (full) return;
385
  // Never track InsightUX's own injected UI (the "press S/E" pill,
386
+ // the gaze-dot canvas, the mouse tracker panel) as if it were page content.
387
+ if (el.id === '__insightux_pill' || el.id === '__insightux_canvas' || el.id === '__insightux_mouse_panel') return;
388
+ if (el.closest && el.closest('#__insightux_pill, #__insightux_canvas, #__insightux_mouse_panel')) return;
389
  const tag = el.tagName.toLowerCase();
390
  const explicit = !!(el.dataset && el.dataset.aoi);
391
  if (!explicit){
 
661
  updatePanel();
662
  }, 2000);
663
 
664
+ // ---- exclude InsightUX's own injected UI from every measurement ----
665
+ // (the status pill, the mouse-tracker panel, the heatmap canvas) β€” only
666
+ // real website elements should ever show up in trail/heatmap/dwell/clicks.
667
+ function isOwnUI(el){
668
+ if (!el) return false;
669
+ if (el.id === '__insightux_pill' || el.id === '__insightux_canvas' || el.id === '__insightux_mouse_panel') return true;
670
+ return !!(el.closest && el.closest('#__insightux_pill, #__insightux_canvas, #__insightux_mouse_panel'));
671
+ }
672
+
673
  // ---- mouse position + hover tracking ----
674
  function updateMousePos(e){
675
+ if (isOwnUI(e.target)) return;
676
  lastClientX = e.clientX; lastClientY = e.clientY;
677
  if (lastPageX === 0) lastPageX = e.pageX;
678
  if (lastPageY === 0) lastPageY = e.pageY;
 
690
  document.addEventListener('click', function(e){
691
  if (!isTracking) return;
692
  if (e.target === canvas) return;
693
+ if (isOwnUI(e.target)) return;
694
  if (!isInteractable(e.target)) return;
695
 
696
  const x = e.pageX, y = e.pageY;
 
988
 
989
 
990
  # =============================================================================
991
+ # API exposed to JS β€” start/stop/quit are the entry points
992
  # =============================================================================
993
 
994
+ # Pages tracking must never start on, checked against window.get_current_url()
995
+ # as a Python-side backstop to CONTROL_JS's isTrackable check (belt and
996
+ # suspenders β€” the JS guard can't run before the JS bridge is up).
997
+ _NON_TRACKABLE_URL_MARKERS = ("insightux_landing.html", "analysis_report.html")
998
+
999
+
1000
  class Api:
1001
  def __init__(self):
1002
  self.window = None
 
1011
  if self.tracking:
1012
  print("[browser_session] already tracking, ignoring")
1013
  return False
1014
+ try:
1015
+ current_url = self.window.get_current_url() or ""
1016
+ except Exception:
1017
+ current_url = ""
1018
+ if any(marker in current_url for marker in _NON_TRACKABLE_URL_MARKERS):
1019
+ print(f"[browser_session] refusing to start on non-website page: {current_url}")
1020
+ self._set_status("Start tracking only works on a website β€” search or open a page first.")
1021
+ return False
1022
  if not os.path.exists(CALIBRATION_PATH):
1023
  print(f"[browser_session] no {CALIBRATION_PATH} found β€” run calibrate.py first")
1024
  self._set_status("No calibration.pkl found β€” run calibrate.py first.")
 
1061
  self.mouse_close_timer.start()
1062
  return True
1063
 
1064
+ def quit_app(self):
1065
+ # X is an emergency kill switch, not a graceful shutdown β€” the user
1066
+ # wants the terminal process gone immediately, camera/threads and all.
1067
+ print("[browser_session] quit_app() called from JS -- terminating process")
1068
+ os._exit(0)
1069
+
1070
  def _inject_tracking_overlay(self):
1071
  try:
1072
  self.window.evaluate_js(TRACKING_JS)