Aryaman25 Claude Sonnet 5 commited on
Commit
d7cdb29
·
1 Parent(s): 0f6fea3

Fix browser/report navigation split, contain session-list scroll, add rename

Browse files

Root-caused "Back randomly opens the Report" bug: the report's Back button
was navigating via location.href to the tracked page's URL, which pushes a
*second* history entry for that same page (report was reached via a real
prior navigation already, so this created a duplicate). The next press of
the toolbar's own Back button then replayed straight into the report. Back
to Browser now calls history.back() first (correct semantics, no
duplicate entry, scroll position preserved) and only falls back to the
recorded tracked-page URL / referrer if back() genuinely had nowhere to go
(checked via a timeout, not assumed). Home was already unconditional
(direct navigation to the landing page, independent of history) and
needed no change. Application-level navigation (Home/Report) and the
embedded browser's own website history remain two separate systems, as
intended.

Recent Sessions on the landing page no longer expands the whole page:
the outer page is now fixed (overflow hidden), and only the session list
itself scrolls, capped to a responsive max-height. Raised the listed
session count from 5 to 25 so the scroll is actually exercised.

Sessions can now be renamed inline (pencil icon -> edit -> Enter saves /
Escape cancels), with the custom label stored in localStorage keyed by
the session's own timestamp-folder id -- purely a display override, never
touching the session identifier, its report file, or which report a
session opens.

No backend file was touched -- git diff confirms changes are confined to
browser_session.py (landing page + toolbar chrome) and analysis.py (report
template) only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files changed (2) hide show
  1. analysis.py +25 -13
  2. browser_session.py +110 -19
analysis.py CHANGED
@@ -666,23 +666,35 @@ document.getElementById('btnFilter').addEventListener('click', function(){
666
  input.focus();
667
  });
668
 
669
- // ---------- Back to Browser: navigates straight back to the website that
670
- // was actually being tracked (same URL session_summary() already recorded
671
- // for this session — the __URL__ shown in the header above) rather than
672
- // leaning on window.history, whose stack can be unreliable to reason about
673
- // inside an embedded webview after a Python-triggered navigation. Falls
674
- // back to real history/referrer only if that URL is unknown. Pure
675
- // client-side navigation — never touches the Api or tracking state. ----------
 
 
 
 
 
 
 
676
  const SESSION_URL = __URL_JSON__;
677
  document.getElementById('backNav').addEventListener('click', function(){
678
- if (SESSION_URL) {
679
- window.location.href = SESSION_URL;
680
- } else if (window.history.length > 1) {
 
 
 
 
681
  window.history.back();
682
- } else if (document.referrer) {
683
- window.location.href = document.referrer;
 
684
  } else {
685
- showToast('No previous page to return to.', true);
686
  }
687
  });
688
 
 
666
  input.focus();
667
  });
668
 
669
+ // ---------- Back to Browser: the report was reached by a REAL navigation
670
+ // (Python's window.load_url() away from the tracked page, once the session
671
+ // ended) — that's already a normal entry in this webview's own history, one
672
+ // step behind this page. history.back() is therefore both correct AND the
673
+ // only option that doesn't add a new entry: jumping straight to SESSION_URL
674
+ // via location.href instead would push a *second*, duplicate visit to that
675
+ // same page, and the very next press of the toolbar's own Back button would
676
+ // then land on the report again (the exact "Back randomly opens the report"
677
+ // bug this replaces). history.back() is tried first for that reason; the
678
+ // recorded tracked-page URL / document.referrer are only a fallback, and
679
+ // only used if history.back() genuinely didn't navigate anywhere (checked
680
+ // via a short timeout) — e.g. the report was opened with no prior page in
681
+ // this window's history at all. Pure client-side navigation either way —
682
+ // never touches the Api or tracking state. ----------
683
  const SESSION_URL = __URL_JSON__;
684
  document.getElementById('backNav').addEventListener('click', function(){
685
+ function fallback(){
686
+ if (SESSION_URL) window.location.href = SESSION_URL;
687
+ else if (document.referrer) window.location.href = document.referrer;
688
+ else showToast('No previous page to return to.', true);
689
+ }
690
+ if (window.history.length > 1) {
691
+ const before = window.location.href;
692
  window.history.back();
693
+ setTimeout(function(){
694
+ if (window.location.href === before) fallback(); // back() had nowhere to go
695
+ }, 400);
696
  } else {
697
+ fallback();
698
  }
699
  });
700
 
browser_session.py CHANGED
@@ -125,13 +125,15 @@ __THEME_CSS__
125
  html, body { height: 100%; }
126
  body {
127
  margin: 0; display: flex; flex-direction: column;
128
- align-items: center; justify-content: center; gap: 26px;
129
  background:
130
  radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--iux-primary) 20%, transparent), transparent 42%),
131
  radial-gradient(circle at 82% 82%, color-mix(in srgb, var(--iux-indigo) 18%, transparent), transparent 45%),
132
  var(--iux-bg);
133
  font-family: var(--iux-font); color: var(--iux-text);
134
  transition: background 0.3s var(--iux-ease), color 0.3s var(--iux-ease);
 
 
135
  }
136
  .theme-toggle {
137
  position: fixed; top: 20px; right: 22px; width: 38px; height: 38px; padding: 0;
@@ -175,21 +177,43 @@ __THEME_CSS__
175
  padding: 1px 7px; font: 700 11px var(--iux-font); color: var(--iux-primary-light);
176
  }
177
 
178
- .recent { width: 640px; max-width: 90vw; margin-top: 4px; }
 
 
 
 
 
 
 
179
  .recent h3 {
180
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--iux-text-faint);
181
- margin: 0 0 10px 2px; font-weight: 600;
 
 
 
 
 
182
  }
183
- .recent-list { display: flex; flex-direction: column; gap: 6px; }
184
  .recent-item {
185
  display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-radius: 10px;
186
- color: var(--iux-text-dim); font-size: 12.5px; transition: background .12s ease;
187
  }
188
  .recent-item.clickable { cursor: pointer; }
189
  .recent-item:hover { background: var(--iux-surface-hi); }
190
  .recent-icon { flex-shrink: 0; color: var(--iux-primary-light); display: flex; }
191
  .recent-info { flex: 1 1 auto; min-width: 0; }
192
- .recent-label { color: var(--iux-text); font-weight: 600; font-size: 12.5px; }
 
 
 
 
 
 
 
 
 
 
 
193
  .recent-domain {
194
  color: var(--iux-text-dim); font-size: 11.5px; margin-top: 2px;
195
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
@@ -203,6 +227,13 @@ __THEME_CSS__
203
  .recent-item .rt { color: var(--iux-text-faint); margin-left: auto; font-size: 11px; flex-shrink: 0; }
204
  .recent-empty { color: var(--iux-text-faint); font-size: 12px; text-align: center; padding: 14px 0; }
205
 
 
 
 
 
 
 
 
206
  .hint {
207
  color: var(--iux-text-faint); text-align: center; line-height: 1.6; font-size: 11.5px; max-width: 520px;
208
  }
@@ -269,19 +300,42 @@ __THEME_JS__
269
 
270
  const RECENT_SESSIONS = __RECENT_SESSIONS_JSON__;
271
  const recentList = document.getElementById('recentList');
272
- if (!RECENT_SESSIONS.length) {
273
- recentList.innerHTML = '<div class="recent-empty">No sessions yet — start tracking on any page to create your first one.</div>';
274
- } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  recentList.innerHTML = RECENT_SESSIONS.map(function(s, i){
276
  const stats = [];
277
  if (s.duration != null) stats.push(s.duration + 's');
278
  if (s.samples != null) stats.push(s.samples + ' gaze samples');
279
  const hasReport = !!s.reportUrl;
 
280
  return '<div class="recent-item' + (hasReport ? ' clickable' : '') + '" data-idx="' + i + '" title="' +
281
  (hasReport ? 'Open the report for this session' : 'Report not available for this session') + '">' +
282
  '<span class="recent-icon">' + iuxIcon('clock', 16) + '</span>' +
283
  '<div class="recent-info">' +
284
- '<div class="recent-label">' + s.label + '</div>' +
 
 
 
 
285
  (s.domain ? '<div class="recent-domain">' + s.domain + '</div>' : '') +
286
  (stats.length ? '<div class="recent-stats">' + stats.join(' &middot; ') + '</div>' : '') +
287
  '</div>' +
@@ -290,13 +344,49 @@ __THEME_JS__
290
  : '<span class="rt">' + s.when + '</span>') +
291
  '</div>';
292
  }).join('');
293
- recentList.querySelectorAll('.recent-item.clickable').forEach(function(el){
294
- el.addEventListener('click', function(){
295
- const s = RECENT_SESSIONS[parseInt(el.dataset.idx, 10)];
296
- if (s && s.reportUrl) location.href = s.reportUrl;
297
- });
 
 
 
 
 
298
  });
299
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  </script>
301
  </body>
302
  </html>
@@ -328,11 +418,11 @@ def _recent_sessions_json(limit=5):
328
  ts = _dt.datetime.strptime(name, "%Y%m%d_%H%M%S")
329
  except ValueError:
330
  continue
331
- entries.append((ts, full))
332
- entries.sort(key=lambda pair: pair[0], reverse=True)
333
 
334
  out = []
335
- for ts, full in entries[:limit]:
336
  report_path = os.path.join(full, "analysis_report.html")
337
  report_url = None
338
  if os.path.exists(report_path):
@@ -350,6 +440,7 @@ def _recent_sessions_json(limit=5):
350
  pass # malformed/partial session folder — still list it, just without stats
351
 
352
  out.append({
 
353
  "label": ts.strftime("%b %d, %Y — %I:%M %p").replace(" 0", " "),
354
  "when": ts.strftime("%Y-%m-%d %H:%M"),
355
  "domain": domain,
@@ -372,7 +463,7 @@ def _write_landing_page():
372
  html = html.replace("__STOP_ICON__", theme.icon("stop", 13))
373
  html = html.replace("__LAYERS_ICON__", theme.icon("layers", 13))
374
  html = html.replace("__CURSOR_ICON__", theme.icon("cursor", 13))
375
- html = html.replace("__RECENT_SESSIONS_JSON__", json.dumps(_recent_sessions_json()))
376
  with open(path, "w", encoding="utf-8") as f:
377
  f.write(html)
378
  return "file://" + path.replace(os.sep, "/")
 
125
  html, body { height: 100%; }
126
  body {
127
  margin: 0; display: flex; flex-direction: column;
128
+ align-items: center; justify-content: center; gap: 22px;
129
  background:
130
  radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--iux-primary) 20%, transparent), transparent 42%),
131
  radial-gradient(circle at 82% 82%, color-mix(in srgb, var(--iux-indigo) 18%, transparent), transparent 45%),
132
  var(--iux-bg);
133
  font-family: var(--iux-font); color: var(--iux-text);
134
  transition: background 0.3s var(--iux-ease), color 0.3s var(--iux-ease);
135
+ overflow: hidden; /* the page itself never scrolls — only .recent-list does */
136
+ padding: 16px 0;
137
  }
138
  .theme-toggle {
139
  position: fixed; top: 20px; right: 22px; width: 38px; height: 38px; padding: 0;
 
177
  padding: 1px 7px; font: 700 11px var(--iux-font); color: var(--iux-primary-light);
178
  }
179
 
180
+ /* Recent Sessions is the only part of this page that scrolls — capped to
181
+ a viewport-relative height so the page itself never grows past the
182
+ window, however many past sessions exist. */
183
+ .recent {
184
+ width: 640px; max-width: 90vw; margin-top: 4px;
185
+ display: flex; flex-direction: column; min-height: 0;
186
+ max-height: min(340px, 34vh);
187
+ }
188
  .recent h3 {
189
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--iux-text-faint);
190
+ margin: 0 0 10px 2px; font-weight: 600; flex-shrink: 0;
191
+ }
192
+ .recent-list {
193
+ display: flex; flex-direction: column; gap: 6px;
194
+ overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth;
195
+ padding-right: 4px; margin-right: -4px;
196
  }
 
197
  .recent-item {
198
  display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-radius: 10px;
199
+ color: var(--iux-text-dim); font-size: 12.5px; transition: background .12s ease; flex-shrink: 0;
200
  }
201
  .recent-item.clickable { cursor: pointer; }
202
  .recent-item:hover { background: var(--iux-surface-hi); }
203
  .recent-icon { flex-shrink: 0; color: var(--iux-primary-light); display: flex; }
204
  .recent-info { flex: 1 1 auto; min-width: 0; }
205
+ .recent-label-row { display: flex; align-items: center; gap: 6px; }
206
+ .recent-label {
207
+ color: var(--iux-text); font-weight: 600; font-size: 12.5px;
208
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
209
+ }
210
+ .recent-timestamp { color: var(--iux-text-faint); font-size: 11px; margin-top: 1px; }
211
+ .recent-edit-btn {
212
+ flex-shrink: 0; width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center;
213
+ justify-content: center; color: var(--iux-text-faint); opacity: 0; transition: opacity .12s ease, color .12s ease;
214
+ }
215
+ .recent-item:hover .recent-edit-btn { opacity: 1; }
216
+ .recent-edit-btn:hover { color: var(--iux-primary-light); background: var(--iux-surface); }
217
  .recent-domain {
218
  color: var(--iux-text-dim); font-size: 11.5px; margin-top: 2px;
219
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
 
227
  .recent-item .rt { color: var(--iux-text-faint); margin-left: auto; font-size: 11px; flex-shrink: 0; }
228
  .recent-empty { color: var(--iux-text-faint); font-size: 12px; text-align: center; padding: 14px 0; }
229
 
230
+ .recent-name-edit { display: flex; align-items: center; gap: 6px; }
231
+ .recent-name-input {
232
+ flex: 1 1 auto; min-width: 0; padding: 5px 9px; font-size: 12.5px; border-radius: 7px;
233
+ border: 1.5px solid var(--iux-primary-light); background: var(--iux-surface); color: var(--iux-text); outline: none;
234
+ }
235
+ .recent-name-edit .iux-btn { flex-shrink: 0; width: 24px; height: 24px; padding: 0; }
236
+
237
  .hint {
238
  color: var(--iux-text-faint); text-align: center; line-height: 1.6; font-size: 11.5px; max-width: 520px;
239
  }
 
300
 
301
  const RECENT_SESSIONS = __RECENT_SESSIONS_JSON__;
302
  const recentList = document.getElementById('recentList');
303
+
304
+ // Custom session display names live only in this browser's localStorage,
305
+ // keyed by each session's own timestamp-folder id — never sent anywhere,
306
+ // never touches the session/report files themselves. Renaming only ever
307
+ // changes what's shown here; s.id (and therefore s.reportUrl) is untouched.
308
+ function loadSessionNames(){
309
+ try { return JSON.parse(localStorage.getItem('__iux_session_names__') || '{}'); } catch (e) { return {}; }
310
+ }
311
+ function saveSessionName(id, name){
312
+ const names = loadSessionNames();
313
+ const trimmed = (name || '').trim();
314
+ if (trimmed) names[id] = trimmed; else delete names[id];
315
+ try { localStorage.setItem('__iux_session_names__', JSON.stringify(names)); } catch (e) {}
316
+ }
317
+
318
+ function renderRecentSessions(){
319
+ if (!RECENT_SESSIONS.length) {
320
+ recentList.innerHTML = '<div class="recent-empty">No sessions yet — start tracking on any page to create your first one.</div>';
321
+ return;
322
+ }
323
+ const names = loadSessionNames();
324
  recentList.innerHTML = RECENT_SESSIONS.map(function(s, i){
325
  const stats = [];
326
  if (s.duration != null) stats.push(s.duration + 's');
327
  if (s.samples != null) stats.push(s.samples + ' gaze samples');
328
  const hasReport = !!s.reportUrl;
329
+ const customName = names[s.id];
330
  return '<div class="recent-item' + (hasReport ? ' clickable' : '') + '" data-idx="' + i + '" title="' +
331
  (hasReport ? 'Open the report for this session' : 'Report not available for this session') + '">' +
332
  '<span class="recent-icon">' + iuxIcon('clock', 16) + '</span>' +
333
  '<div class="recent-info">' +
334
+ '<div class="recent-label-row" id="recentRow' + i + '">' +
335
+ '<span class="recent-label" id="recentLabel' + i + '">' + (customName || s.label) + '</span>' +
336
+ '<button type="button" class="recent-edit-btn" id="recentEdit' + i + '" title="Rename session">' + iuxIcon('type', 12) + '</button>' +
337
+ '</div>' +
338
+ (customName ? '<div class="recent-timestamp">' + s.label + '</div>' : '') +
339
  (s.domain ? '<div class="recent-domain">' + s.domain + '</div>' : '') +
340
  (stats.length ? '<div class="recent-stats">' + stats.join(' &middot; ') + '</div>' : '') +
341
  '</div>' +
 
344
  : '<span class="rt">' + s.when + '</span>') +
345
  '</div>';
346
  }).join('');
347
+
348
+ RECENT_SESSIONS.forEach(function(s, i){
349
+ const item = recentList.children[i];
350
+ if (item && s.reportUrl) {
351
+ item.addEventListener('click', function(){ location.href = s.reportUrl; });
352
+ }
353
+ const editBtn = document.getElementById('recentEdit' + i);
354
+ if (editBtn) {
355
+ editBtn.addEventListener('click', function(e){ e.stopPropagation(); startEditingName(i); });
356
+ }
357
  });
358
  }
359
+
360
+ function startEditingName(i){
361
+ const s = RECENT_SESSIONS[i];
362
+ const names = loadSessionNames();
363
+ const current = names[s.id] || '';
364
+ const row = document.getElementById('recentRow' + i);
365
+ row.innerHTML =
366
+ '<div class="recent-name-edit">' +
367
+ '<input type="text" class="recent-name-input" id="recentNameInput' + i + '" value="' +
368
+ current.replace(/"/g, '&quot;') + '" placeholder="' + s.label + '" maxlength="80">' +
369
+ '<button type="button" class="iux-btn" id="recentNameSave' + i + '" title="Save">' + iuxIcon('check', 13) + '</button>' +
370
+ '<button type="button" class="iux-btn" id="recentNameCancel' + i + '" title="Cancel">' + iuxIcon('x', 13) + '</button>' +
371
+ '</div>';
372
+ const input = document.getElementById('recentNameInput' + i);
373
+ input.addEventListener('click', function(e){ e.stopPropagation(); });
374
+ input.focus();
375
+ input.select();
376
+
377
+ function commit(){ saveSessionName(s.id, input.value); renderRecentSessions(); }
378
+ function cancel(){ renderRecentSessions(); }
379
+
380
+ document.getElementById('recentNameSave' + i).addEventListener('click', function(e){ e.stopPropagation(); commit(); });
381
+ document.getElementById('recentNameCancel' + i).addEventListener('click', function(e){ e.stopPropagation(); cancel(); });
382
+ input.addEventListener('keydown', function(e){
383
+ e.stopPropagation();
384
+ if (e.key === 'Enter') { e.preventDefault(); commit(); }
385
+ else if (e.key === 'Escape') { e.preventDefault(); cancel(); }
386
+ });
387
+ }
388
+
389
+ renderRecentSessions();
390
  </script>
391
  </body>
392
  </html>
 
418
  ts = _dt.datetime.strptime(name, "%Y%m%d_%H%M%S")
419
  except ValueError:
420
  continue
421
+ entries.append((ts, name, full))
422
+ entries.sort(key=lambda triple: triple[0], reverse=True)
423
 
424
  out = []
425
+ for ts, name, full in entries[:limit]:
426
  report_path = os.path.join(full, "analysis_report.html")
427
  report_url = None
428
  if os.path.exists(report_path):
 
440
  pass # malformed/partial session folder — still list it, just without stats
441
 
442
  out.append({
443
+ "id": name, # stable session identifier (its own timestamp folder name) — never shown, only used to key a display-name override
444
  "label": ts.strftime("%b %d, %Y — %I:%M %p").replace(" 0", " "),
445
  "when": ts.strftime("%Y-%m-%d %H:%M"),
446
  "domain": domain,
 
463
  html = html.replace("__STOP_ICON__", theme.icon("stop", 13))
464
  html = html.replace("__LAYERS_ICON__", theme.icon("layers", 13))
465
  html = html.replace("__CURSOR_ICON__", theme.icon("cursor", 13))
466
+ html = html.replace("__RECENT_SESSIONS_JSON__", json.dumps(_recent_sessions_json(limit=25)))
467
  with open(path, "w", encoding="utf-8") as f:
468
  f.write(html)
469
  return "file://" + path.replace(os.sep, "/")