Fix browser/report navigation split, contain session-list scroll, add rename
Browse filesRoot-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>
- analysis.py +25 -13
- browser_session.py +110 -19
|
@@ -666,23 +666,35 @@ document.getElementById('btnFilter').addEventListener('click', function(){
|
|
| 666 |
input.focus();
|
| 667 |
});
|
| 668 |
|
| 669 |
-
// ---------- Back to Browser:
|
| 670 |
-
//
|
| 671 |
-
//
|
| 672 |
-
//
|
| 673 |
-
//
|
| 674 |
-
//
|
| 675 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
const SESSION_URL = __URL_JSON__;
|
| 677 |
document.getElementById('backNav').addEventListener('click', function(){
|
| 678 |
-
|
| 679 |
-
window.location.href = SESSION_URL;
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
window.history.back();
|
| 682 |
-
|
| 683 |
-
|
|
|
|
| 684 |
} else {
|
| 685 |
-
|
| 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 |
|
|
@@ -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:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
(s.domain ? '<div class="recent-domain">' + s.domain + '</div>' : '') +
|
| 286 |
(stats.length ? '<div class="recent-stats">' + stats.join(' · ') + '</div>' : '') +
|
| 287 |
'</div>' +
|
|
@@ -290,13 +344,49 @@ __THEME_JS__
|
|
| 290 |
: '<span class="rt">' + s.when + '</span>') +
|
| 291 |
'</div>';
|
| 292 |
}).join('');
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 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
|
| 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(' · ') + '</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, '"') + '" 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, "/")
|