Skip to content

Instantly share code, notes, and snippets.

@mark05e
Last active September 20, 2026 23:40
Show Gist options
  • Select an option

  • Save mark05e/d8328551c85497211316e9f43511fd98 to your computer and use it in GitHub Desktop.

Select an option

Save mark05e/d8328551c85497211316e9f43511fd98 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Toggl Track Daily Summary Panel
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Adds a static floating panel to view daily Toggl time summaries.
// @author mark05e
// @match https://track.toggl.com/*
// @updateURL https://gist.github.com/mark05e/d8328551c85497211316e9f43511fd98/raw
// @downloadURL https://gist.github.com/mark05e/d8328551c85497211316e9f43511fd98/raw
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- API layer ---
async function fetchTogglEntries(before) {
const url = new URL('https://track.toggl.com/api/v9/me/time_entries');
url.searchParams.set('meta', 'true');
url.searchParams.set('include_sharing', 'true');
url.searchParams.set('include_expenses', 'true');
if (before) url.searchParams.set('before', before);
const res = await fetch(url, { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error(`Toggl API error: ${res.status}`);
return res.json();
}
// --- UI layer ---
async function showTogglPanel() {
const existing = document.getElementById('toggl-panel');
if (existing) {
existing.style.display = existing.style.display === 'none' ? 'flex' : 'none';
return;
}
const entriesById = new Map();
let idx = 0;
function regroup() {
const byDay = {};
for (const e of entriesById.values()) {
const d = new Date(e.start).toLocaleDateString('en-CA');
(byDay[d] ??= {})[e.description || '(no description)'] =
(byDay[d]?.[e.description || '(no description)'] || 0) + e.duration;
}
return Object.keys(byDay).sort().reverse();
}
const byDayLookup = () => {
const byDay = {};
for (const e of entriesById.values()) {
const d = new Date(e.start).toLocaleDateString('en-CA');
(byDay[d] ??= {})[e.description || '(no description)'] =
(byDay[d]?.[e.description || '(no description)'] || 0) + e.duration;
}
return byDay;
};
const fmt = s => `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m ${s % 60}s`;
const weekday = d => {
const [y, m, day] = d.split('-').map(Number);
return new Date(y, m - 1, day).toLocaleDateString('en-US', { weekday: 'long' });
};
// Main static panel configuration
const panel = document.createElement('div');
panel.id = 'toggl-panel';
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 380px;
height: 320px;
background: #1e1e2e;
color: #f0f0f0;
padding: 20px;
border-radius: 12px;
font-family: system-ui, -apple-system, sans-serif;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
z-index: 99999;
display: flex;
flex-direction: column;
box-sizing: border-box;
`;
document.body.appendChild(panel);
panel.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; flex-shrink:0;">
<button id="prevBtn" style="cursor:pointer; background:#2d2d3d; color:#fff; border:none; padding:4px 8px; border-radius:4px;">◀</button>
<select id="dateSelect" style="flex:1; margin:0 8px; background:#2d2d3d; color:#fff; border:1px solid #444; padding:4px; border-radius:4px; text-overflow:ellipsis;">
<option>Loading dates...</option>
</select>
<button id="nextBtn" style="cursor:pointer; background:#2d2d3d; color:#fff; border:none; padding:4px 8px; border-radius:4px;">▶</button>
<button id="closeBtn" style="margin-left:8px; cursor:pointer; background:transparent; color:#888; border:none; font-size:16px;">✕</button>
</div>
<div id="body" style="flex:1; overflow-y:auto; padding-right:4px;">
<p style="color:#aaa; text-align:center; margin-top:40px;">Fetching Toggl data...</p>
</div>
<div style="border-top:1px solid #333; padding-top:8px; margin-top:8px; display:flex; justify-content:space-between; flex-shrink:0;">
<strong>Total</strong>
<strong id="totalVal">0h 0m 0s</strong>
</div>
`;
panel.querySelector('#closeBtn').onclick = () => { panel.style.display = 'none'; };
try {
const initial = await fetchTogglEntries();
for (const e of initial) entriesById.set(e.id, e);
} catch (err) {
panel.querySelector('#body').innerHTML = `<p style="color:#ff6b6b;">Error loading data</p>`;
return;
}
let byDayDates = regroup();
async function loadMore() {
panel.querySelector('#body').innerHTML = '<p style="color:#aaa; text-align:center;">Loading more dates…</p>';
const oldest = byDayDates[byDayDates.length - 1];
const older = await fetchTogglEntries(oldest);
for (const e of older) entriesById.set(e.id, e);
byDayDates = regroup();
render();
}
function render() {
if (!byDayDates.length) {
panel.querySelector('#body').innerHTML = '<p>No time entries found.</p>';
return;
}
const day = byDayDates[idx];
const descs = byDayLookup()[day] || {};
const total = Object.values(descs).reduce((a, b) => a + b, 0);
const rows = Object.entries(descs)
.map(([desc, sec]) => `
<tr>
<td style="padding:6px 0; border-bottom:1px solid #2a2a3a; word-break:break-word;">${desc}</td>
<td style="text-align:right; padding-left:12px; border-bottom:1px solid #2a2a3a; white-space:nowrap; color:#e0e0e0;">${fmt(sec)}</td>
</tr>`)
.join('');
const select = panel.querySelector('#dateSelect');
select.innerHTML = `
${byDayDates.map((d, i) => `<option value="${i}" ${i === idx ? 'selected' : ''}>${d} (${weekday(d)})</option>`).join('')}
<option value="loadmore">— Load more… —</option>
`;
panel.querySelector('#body').innerHTML = `<table style="width:100%; border-collapse:collapse; font-size:13px;">${rows}</table>`;
panel.querySelector('#totalVal').textContent = fmt(total);
panel.querySelector('#prevBtn').onclick = () => { idx = Math.max(0, idx - 1); render(); };
panel.querySelector('#nextBtn').onclick = () => { idx = Math.min(byDayDates.length - 1, idx + 1); render(); };
select.onchange = e => {
if (e.target.value === 'loadmore') loadMore();
else { idx = Number(e.target.value); render(); }
};
}
render();
}
// --- Add Trigger Floating Button ---
function addFloatingButton() {
const btn = document.createElement('button');
btn.id = 'toggl-summary-trigger';
btn.innerText = '⏱ Summary';
btn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99998;
background: #e50b20;
color: #ffffff;
border: none;
padding: 10px 16px;
border-radius: 20px;
font-weight: bold;
font-family: system-ui, -apple-system, sans-serif;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: transform 0.2s ease, background-color 0.2s ease;
`;
btn.onmouseover = () => { btn.style.transform = 'scale(1.05)'; };
btn.onmouseout = () => { btn.style.transform = 'scale(1)'; };
btn.onclick = showTogglPanel;
document.body.appendChild(btn);
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
addFloatingButton();
} else {
window.addEventListener('DOMContentLoaded', addFloatingButton);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment