Last active
September 20, 2026 23:40
-
-
Save mark05e/d8328551c85497211316e9f43511fd98 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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