Skip to content

Instantly share code, notes, and snippets.

@YuriyGuts
Last active March 13, 2026 11:59
Show Gist options
  • Select an option

  • Save YuriyGuts/e62000e2867bf5f9e7d094754d3f9f35 to your computer and use it in GitHub Desktop.

Select an option

Save YuriyGuts/e62000e2867bf5f9e7d094754d3f9f35 to your computer and use it in GitHub Desktop.
Logs the inputs and outputs of all tool calls Claude Code makes. Provides a friendly browser-based interface for reviewing the tool call logs. One static HTML file, no frameworks, no build process.

Claude Code Tool Call Log Viewer

Logs the inputs and outputs of all tool calls Claude Code makes. Provides a friendly browser-based interface for reviewing the tool call logs. One static HTML file, no frameworks, no build process.

Installation

Configure the PostToolUse hook in your ~/.claude/settings.json to record all tool calls to ~/.claude/tool-calls.log:

{
  # ...

  "hooks": {
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{timestamp: now | strftime(\"%Y-%m-%d %H:%M:%S\"), tool: .tool_name, project: .cwd, session: .session_id, input: .tool_input, output: .tool_response}' >> ~/.claude/tool-calls.log"
          }
        ]
      }
    ]
  }
}

Note: jq must be available in your PATH.

Usage

  1. Start a Claude Code session and do some work.
  2. Open tool-call-log-viewer.html in your browser.
  3. Press the "Load Log File" button and upload the ~/.claude/tool-calls.log file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tool Call Log Viewer</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #232529;
color: #cbcfd0;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #cbcfd0;
}
/* Top controls */
.controls {
background: #282a30;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.file-input-wrapper {
position: relative;
}
.file-input-wrapper input[type="file"] {
display: none;
}
.file-input-wrapper label,
button {
background: #5fc9be;
color: #232529;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
border: none;
font-size: 14px;
transition: background 0.2s;
}
.file-input-wrapper label:hover,
button:hover {
background: #6cdfd3;
}
button.clear-btn {
background: #cc6155;
color: #e4e9ea;
}
button.clear-btn:hover {
background: #ea695b;
}
button.quick-filter {
background: transparent;
border: 1px solid #414547;
color: #9a9a9a;
}
button.quick-filter:hover {
background: #414547;
color: #e4e9ea;
}
button.quick-filter.active {
background: rgba(95, 201, 190, 0.15);
border-color: #5fc9be;
color: #6cdfd3;
}
.search-box {
flex: 1;
min-width: 200px;
}
.search-box input {
width: 100%;
padding: 10px 15px;
border-radius: 5px;
border: 1px solid #414547;
background: #1c1d21;
color: #cbcfd0;
font-size: 14px;
}
.search-box input:focus {
outline: none;
border-color: #5fc9be;
}
/* Filters row */
.filters {
background: #282a30;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-group label {
font-size: 14px;
color: #9a9a9a;
}
.filter-group select,
.filter-group input[type="text"] {
padding: 8px 12px;
border-radius: 5px;
border: 1px solid #414547;
background: #1c1d21;
color: #cbcfd0;
font-size: 14px;
font-family: monospace;
}
.filter-group select:focus,
.filter-group input:focus {
outline: none;
border-color: #5fc9be;
}
/* Tool checkboxes */
.tool-filters-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.tool-filters-actions {
display: flex;
gap: 8px;
}
.tool-filters-actions button {
padding: 4px 10px;
font-size: 12px;
background: transparent;
border: 1px solid #414547;
border-radius: 4px;
color: #9a9a9a;
cursor: pointer;
}
.tool-filters-actions button:hover {
background: #414547;
color: #e4e9ea;
}
.tool-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tool-checkbox {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
background: #1c1d21;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
}
.tool-checkbox:hover {
background: #2a2b30;
}
.tool-checkbox input {
cursor: pointer;
margin: 0;
flex-shrink: 0;
}
/* Stats bar */
.stats {
background: #1c1d21;
padding: 10px 20px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
color: #9a9a9a;
}
/* Drop zone */
.drop-zone {
border: 2px dashed #414547;
border-radius: 8px;
padding: 40px;
text-align: center;
margin-bottom: 20px;
transition: all 0.2s;
}
.drop-zone.dragover {
border-color: #5fc9be;
background: rgba(95, 201, 190, 0.1);
}
.drop-zone p {
color: #9a9a9a;
font-size: 16px;
}
/* Log entries */
.entries {
display: flex;
flex-direction: column;
gap: 10px;
}
.entry {
background: #282a30;
border-radius: 8px;
overflow: hidden;
border-left: 4px solid #9a9a9a;
}
/* Tool-specific colors */
.entry.tool-read { border-left-color: #91c382; }
.entry.tool-edit { border-left-color: #cabc5c; }
.entry.tool-write { border-left-color: #cc6155; }
.entry.tool-bash { border-left-color: #cc6078; }
.entry.tool-glob { border-left-color: #6d97ba; }
.entry.tool-grep { border-left-color: #5fc9be; }
.entry.tool-task { border-left-color: #eb6685; }
.entry.tool-webfetch { border-left-color: #7ab3e1; }
.entry.tool-websearch { border-left-color: #6cdfd3; }
.entry-header {
display: flex;
align-items: center;
padding: 12px 15px;
gap: 15px;
cursor: pointer;
flex-wrap: wrap;
}
.entry-header:hover {
background: #414547;
}
.tool-name {
font-weight: 600;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
min-width: 80px;
text-align: center;
line-height: 1.4;
}
.tool-read .tool-name { background: rgba(145, 195, 130, 0.2); color: #a4d992; }
.tool-edit .tool-name { background: rgba(202, 188, 92, 0.2); color: #ebd96a; }
.tool-write .tool-name { background: rgba(204, 97, 85, 0.2); color: #ea695b; }
.tool-bash .tool-name { background: rgba(204, 96, 120, 0.2); color: #eb6685; }
.tool-glob .tool-name { background: rgba(109, 151, 186, 0.2); color: #7ab3e1; }
.tool-grep .tool-name { background: rgba(95, 201, 190, 0.2); color: #6cdfd3; }
.tool-task .tool-name { background: rgba(235, 102, 133, 0.2); color: #eb6685; }
.tool-webfetch .tool-name { background: rgba(122, 179, 225, 0.2); color: #7ab3e1; }
.tool-websearch .tool-name { background: rgba(108, 223, 211, 0.2); color: #6cdfd3; }
.timestamp {
color: #9a9a9a;
font-size: 13px;
font-family: monospace;
line-height: 1.4;
position: relative;
top: 2px;
}
.project-path {
color: #7ab3e1;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
line-height: 1.4;
}
.session-id {
color: #9a9a9a;
font-size: 13px;
font-family: monospace;
line-height: 1.4;
position: relative;
top: 2px;
}
.expand-icon {
margin-left: auto;
color: #9a9a9a;
transition: transform 0.2s;
}
.entry.expanded .expand-icon {
transform: rotate(180deg);
}
.entry-body {
display: none;
padding: 0 15px 15px;
}
.entry.expanded .entry-body {
display: block;
}
.section {
margin-top: 10px;
}
.section-header {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
cursor: pointer;
color: #9a9a9a;
font-size: 13px;
}
.section-header:hover {
color: #e4e9ea;
}
.section-content {
background: #1c1d21;
padding: 12px;
border-radius: 5px;
font-family: 'Consolas', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
.section.collapsed .section-content {
display: none;
}
.section-header .section-icon {
transition: transform 0.2s;
}
.section.collapsed .section-header .section-icon {
transform: rotate(-90deg);
}
.truncated-notice {
color: #cabc5c;
font-style: italic;
margin-top: 8px;
}
.show-more-btn {
background: transparent;
color: #5fc9be;
padding: 5px 10px;
font-size: 12px;
margin-top: 8px;
}
.show-more-btn:hover {
background: rgba(95, 201, 190, 0.1);
}
/* Error message */
.error {
background: rgba(204, 97, 85, 0.2);
border: 1px solid #cc6155;
color: #ea695b;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
/* Empty state */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9a9a9a;
}
.empty-state h2 {
margin-bottom: 10px;
color: #d5d8d9;
}
/* Highlight search matches */
.highlight {
background: rgba(235, 217, 106, 0.3);
padding: 1px 2px;
border-radius: 2px;
}
/* JSON syntax highlighting */
.json-key { color: #7ab3e1; }
.json-string { color: #91c382; }
.json-number { color: #cabc5c; }
.json-boolean { color: #cc6078; }
.json-null { color: #cc6155; }
.json-punctuation { color: #9a9a9a; }
/* Responsive */
@media (max-width: 768px) {
.controls, .filters {
flex-direction: column;
align-items: stretch;
}
.search-box {
width: 100%;
}
.project-path {
max-width: 150px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Tool Call Log Viewer</h1>
<div class="controls">
<div class="file-input-wrapper">
<input type="file" id="fileInput" accept=".log,.jsonl,.txt">
<label for="fileInput">Load Log File</label>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search inputs and outputs...">
</div>
</div>
<div class="filters">
<div class="filter-group">
<label>Tools:</label>
<div class="tool-filters-container">
<div class="tool-filters-actions">
<button id="selectAllTools">All</button>
<button id="selectNoTools">None</button>
</div>
<div class="tool-filters" id="toolFilters">
<!-- Populated dynamically -->
</div>
</div>
</div>
</div>
<div class="filters">
<button class="quick-filter" id="todayFilter">Today</button>
<button class="quick-filter" id="latestSessionFilter">Latest Session</button>
<div class="filter-group">
<label>From:</label>
<input type="text" id="fromDate" placeholder="yyyy-mm-dd HH:MM:SS">
</div>
<div class="filter-group">
<label>To:</label>
<input type="text" id="toDate" placeholder="yyyy-mm-dd HH:MM:SS">
</div>
<button class="clear-btn" id="clearFilters">Clear Filters</button>
</div>
<div class="stats" id="stats">
Load a log file to view tool calls
</div>
<div class="drop-zone" id="dropZone">
<p>Drag and drop a log file here, or use the Load button above</p>
</div>
<div id="errorContainer"></div>
<div class="entries" id="entriesContainer">
<div class="empty-state">
<h2>No entries loaded</h2>
<p>Load a tool-calls.log file to get started</p>
</div>
</div>
</div>
<script>
// State
let allEntries = [];
let filteredEntries = [];
let expandedEntries = new Set();
let expandedSections = new Set();
let showFullContent = new Set();
let activeSessionFilter = null;
// Known tool types
const TOOL_COLORS = {
'Read': 'read',
'Edit': 'edit',
'Write': 'write',
'Bash': 'bash',
'Glob': 'glob',
'Grep': 'grep',
'Task': 'task',
'WebFetch': 'webfetch',
'WebSearch': 'websearch'
};
// DOM elements
const fileInput = document.getElementById('fileInput');
const searchInput = document.getElementById('searchInput');
const toolFilters = document.getElementById('toolFilters');
const fromDate = document.getElementById('fromDate');
const toDate = document.getElementById('toDate');
const clearFilters = document.getElementById('clearFilters');
const todayFilter = document.getElementById('todayFilter');
const latestSessionFilter = document.getElementById('latestSessionFilter');
const stats = document.getElementById('stats');
const dropZone = document.getElementById('dropZone');
const errorContainer = document.getElementById('errorContainer');
const entriesContainer = document.getElementById('entriesContainer');
// Load saved filter preferences
function loadPreferences() {
try {
const prefs = JSON.parse(localStorage.getItem('toolLogViewerPrefs') || '{}');
if (prefs.search) searchInput.value = prefs.search;
if (prefs.fromDate) fromDate.value = prefs.fromDate;
if (prefs.toDate) toDate.value = prefs.toDate;
return prefs.selectedTools || null;
} catch (e) {
return null;
}
}
// Save filter preferences
function savePreferences() {
const selectedTools = Array.from(document.querySelectorAll('.tool-checkbox input:checked'))
.map(cb => cb.value);
localStorage.setItem('toolLogViewerPrefs', JSON.stringify({
search: searchInput.value,
fromDate: fromDate.value,
toDate: toDate.value,
selectedTools
}));
}
// File handling
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) loadFile(file);
});
// Drag and drop
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) loadFile(file);
});
function loadFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
parseLog(e.target.result);
};
reader.onerror = () => {
showError('Failed to read file');
};
reader.readAsText(file);
}
function parseLog(content) {
clearError();
allEntries = [];
const lines = content.split('\n').filter(line => line.trim());
let parseErrors = 0;
lines.forEach((line, index) => {
try {
const entry = JSON.parse(line);
entry._index = index;
allEntries.push(entry);
} catch (e) {
parseErrors++;
}
});
if (allEntries.length === 0) {
showError('No valid JSON entries found in the file');
return;
}
if (parseErrors > 0) {
showError(`Parsed ${allEntries.length} entries, skipped ${parseErrors} invalid lines`, 'warning');
}
dropZone.style.display = 'none';
setupToolFilters();
applyFilters();
}
function setupToolFilters() {
const tools = new Set();
allEntries.forEach(entry => {
if (entry.tool) tools.add(entry.tool);
});
const savedTools = loadPreferences();
toolFilters.innerHTML = '';
Array.from(tools).sort().forEach(tool => {
const label = document.createElement('label');
label.className = 'tool-checkbox';
const checked = savedTools ? savedTools.includes(tool) : true;
label.innerHTML = `
<input type="checkbox" value="${tool}" ${checked ? 'checked' : ''}>
${tool}
`;
toolFilters.appendChild(label);
});
// Add event listeners
toolFilters.querySelectorAll('input').forEach(cb => {
cb.addEventListener('change', () => {
savePreferences();
applyFilters();
});
});
}
function showError(message, type = 'error') {
errorContainer.innerHTML = `<div class="error">${message}</div>`;
}
function clearError() {
errorContainer.innerHTML = '';
}
// Parse date in yyyy-mm-dd HH:MM:SS format
function parseFilterDate(str) {
if (!str) return null;
// Replace space with T for ISO format compatibility
const isoStr = str.trim().replace(' ', 'T');
const date = new Date(isoStr);
return isNaN(date.getTime()) ? null : date.getTime();
}
// Filtering
function applyFilters() {
const searchTerm = searchInput.value.toLowerCase();
const selectedTools = Array.from(document.querySelectorAll('.tool-checkbox input:checked'))
.map(cb => cb.value);
const fromTime = parseFilterDate(fromDate.value);
const toTime = parseFilterDate(toDate.value);
filteredEntries = allEntries.filter(entry => {
// Tool filter - if no tools selected, show nothing
if (!selectedTools.includes(entry.tool)) {
return false;
}
// Time filter
if (entry.timestamp) {
const entryTime = new Date(entry.timestamp).getTime();
if (fromTime && entryTime < fromTime) return false;
if (toTime && entryTime > toTime) return false;
}
// Session filter
if (activeSessionFilter && entry.session !== activeSessionFilter) {
return false;
}
// Search filter
if (searchTerm) {
const inputStr = JSON.stringify(entry.input || '').toLowerCase();
const outputStr = JSON.stringify(entry.output || '').toLowerCase();
const toolStr = (entry.tool || '').toLowerCase();
if (!inputStr.includes(searchTerm) &&
!outputStr.includes(searchTerm) &&
!toolStr.includes(searchTerm)) {
return false;
}
}
return true;
});
updateStats();
renderEntries();
}
function updateStats() {
const toolCounts = {};
filteredEntries.forEach(entry => {
toolCounts[entry.tool] = (toolCounts[entry.tool] || 0) + 1;
});
const toolSummary = Object.entries(toolCounts)
.sort((a, b) => b[1] - a[1])
.map(([tool, count]) => `${tool}: ${count}`)
.join(' | ');
stats.textContent = `Showing ${filteredEntries.length} of ${allEntries.length} entries | ${toolSummary}`;
}
function renderEntries() {
if (filteredEntries.length === 0) {
entriesContainer.innerHTML = `
<div class="empty-state">
<h2>No matching entries</h2>
<p>Try adjusting your filters</p>
</div>
`;
return;
}
const searchTerm = searchInput.value.toLowerCase();
entriesContainer.innerHTML = filteredEntries.map(entry => {
const toolClass = TOOL_COLORS[entry.tool] || '';
const isExpanded = expandedEntries.has(entry._index);
const timestamp = entry.timestamp ? formatTimestamp(entry.timestamp) : 'No timestamp';
const projectPath = entry.project || '';
const projectName = projectPath ? projectPath.split('/').pop() || projectPath : 'Unknown project';
const sessionId = entry.session ? entry.session.substring(0, 8) + '...' : '';
const inputExpanded = !expandedSections.has(`${entry._index}-input`);
const outputExpanded = !expandedSections.has(`${entry._index}-output`);
return `
<div class="entry ${isExpanded ? 'expanded' : ''} tool-${toolClass}" data-index="${entry._index}">
<div class="entry-header" onclick="toggleEntry(${entry._index})">
<span class="tool-name">${entry.tool || 'Unknown'}</span>
<span class="timestamp">${timestamp}</span>
<span class="project-path" title="${projectPath}">${projectName}</span>
<span class="session-id">${sessionId}</span>
<span class="expand-icon">▼</span>
</div>
<div class="entry-body">
<div class="section ${inputExpanded ? '' : 'collapsed'}">
<div class="section-header" onclick="toggleSection(${entry._index}, 'input', event)">
<span class="section-icon">▼</span> Input
</div>
${renderContent(entry.input, entry._index, 'input', searchTerm)}
</div>
<div class="section ${outputExpanded ? '' : 'collapsed'}">
<div class="section-header" onclick="toggleSection(${entry._index}, 'output', event)">
<span class="section-icon">▼</span> Output
</div>
${renderContent(entry.output, entry._index, 'output', searchTerm)}
</div>
</div>
</div>
`;
}).join('');
}
function formatTimestamp(ts) {
try {
const date = new Date(ts);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (e) {
return ts;
}
}
function formatJson(str, searchTerm) {
if (!str || str === 'null') return '<span class="json-null">null</span>';
let highlighted = highlightJson(str);
if (searchTerm) {
highlighted = highlightSearch(highlighted, searchTerm);
}
return highlighted;
}
function highlightJson(str) {
// Escape HTML first, then apply syntax highlighting
const escaped = escapeHtml(str);
// Match JSON tokens and wrap them in spans
// Order matters: strings must be matched before other tokens
return escaped.replace(
/("(?:[^"\\]|\\.)*")(\s*:)?|(\b(?:true|false)\b)|(\bnull\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}\[\],:])/g,
(match, str, colon, bool, nul, num, punct) => {
if (str) {
if (colon) {
// It's a key (string followed by colon)
return `<span class="json-key">${str}</span><span class="json-punctuation">${colon.trim()}</span>${colon.replace(':', '')}`;
}
// It's a string value
return `<span class="json-string">${str}</span>`;
}
if (bool) return `<span class="json-boolean">${bool}</span>`;
if (nul) return `<span class="json-null">${nul}</span>`;
if (num) return `<span class="json-number">${num}</span>`;
if (punct) return `<span class="json-punctuation">${punct}</span>`;
return match;
}
);
}
function highlightSearch(text, term) {
if (!term) return text;
// Search highlighting works on already-highlighted HTML
// We need to match text content while preserving tags
const escapedTerm = escapeRegex(escapeHtml(term));
const regex = new RegExp(`(${escapedTerm})(?![^<]*>)`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const TRUNCATE_LENGTH = 500;
function renderContent(obj, index, type, searchTerm) {
const key = `${index}-${type}`;
const isFull = showFullContent.has(key);
// Get raw JSON string first
const rawJson = getRawJson(obj);
const needsTruncation = rawJson.length > TRUNCATE_LENGTH;
// Truncate raw string BEFORE highlighting to avoid breaking HTML tags
let displayJson = rawJson;
if (needsTruncation && !isFull) {
displayJson = rawJson.substring(0, TRUNCATE_LENGTH);
}
// Apply highlighting after truncation
const displayContent = formatJson(displayJson, searchTerm);
return `
<div class="section-content">${displayContent}</div>
${needsTruncation ? `
<button class="show-more-btn" onclick="toggleFullContent(${index}, '${type}', event)">
${isFull ? 'Show less' : `Show more (${rawJson.length} chars)`}
</button>
` : ''}
`;
}
function getRawJson(obj) {
if (obj === undefined || obj === null) return 'null';
try {
return JSON.stringify(obj, null, 2);
} catch (e) {
return String(obj);
}
}
// Event handlers
window.toggleEntry = function(index) {
if (expandedEntries.has(index)) {
expandedEntries.delete(index);
} else {
expandedEntries.add(index);
}
renderEntries();
};
window.toggleSection = function(index, type, event) {
event.stopPropagation();
const key = `${index}-${type}`;
if (expandedSections.has(key)) {
expandedSections.delete(key);
} else {
expandedSections.add(key);
}
renderEntries();
};
window.toggleFullContent = function(index, type, event) {
event.stopPropagation();
const key = `${index}-${type}`;
if (showFullContent.has(key)) {
showFullContent.delete(key);
} else {
showFullContent.add(key);
}
renderEntries();
};
// Filter event listeners
searchInput.addEventListener('input', debounce(() => {
savePreferences();
applyFilters();
}, 300));
fromDate.addEventListener('input', () => {
todayFilter.classList.remove('active');
savePreferences();
applyFilters();
});
toDate.addEventListener('input', () => {
todayFilter.classList.remove('active');
savePreferences();
applyFilters();
});
clearFilters.addEventListener('click', () => {
searchInput.value = '';
fromDate.value = '';
toDate.value = '';
activeSessionFilter = null;
todayFilter.classList.remove('active');
latestSessionFilter.classList.remove('active');
document.querySelectorAll('.tool-checkbox input').forEach(cb => cb.checked = true);
localStorage.removeItem('toolLogViewerPrefs');
applyFilters();
});
document.getElementById('selectAllTools').addEventListener('click', () => {
document.querySelectorAll('.tool-checkbox input').forEach(cb => cb.checked = true);
savePreferences();
applyFilters();
});
document.getElementById('selectNoTools').addEventListener('click', () => {
document.querySelectorAll('.tool-checkbox input').forEach(cb => cb.checked = false);
savePreferences();
applyFilters();
});
todayFilter.addEventListener('click', () => {
const isActive = todayFilter.classList.contains('active');
if (isActive) {
fromDate.value = '';
toDate.value = '';
todayFilter.classList.remove('active');
} else {
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
fromDate.value = `${yyyy}-${mm}-${dd} 00:00:00`;
toDate.value = `${yyyy}-${mm}-${dd} 23:59:59`;
todayFilter.classList.add('active');
}
savePreferences();
applyFilters();
});
latestSessionFilter.addEventListener('click', () => {
const isActive = latestSessionFilter.classList.contains('active');
if (isActive) {
activeSessionFilter = null;
latestSessionFilter.classList.remove('active');
} else {
if (allEntries.length === 0) return;
const lastEntry = allEntries[allEntries.length - 1];
activeSessionFilter = lastEntry.session || null;
if (!activeSessionFilter) return;
latestSessionFilter.classList.add('active');
}
applyFilters();
});
function debounce(fn, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}
// Initialize
loadPreferences();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment