// Substack versions → per-day ASCII timeline + window-independent total. // Paste into console on a /publish/post/{id} page. (async () => { const CHUNK_MINUTES = 5; // bucket size for both display and totals const START_HOUR = 8; // display window start (local) const END_HOUR = 2; // display window end (next day, local) const ACTIVE_CHAR = "━"; // writing const INACTIVE_CHAR = "·"; // idle const FOLLOW_PREVIOUS = true; // walk previousBucket chain const m = location.pathname.match(/\/publish\/post\/(\d+)/); if (!m) throw new Error("Could not find /publish/post/{id} in URL"); const draftId = m[1]; const mmdd = d => `${String(d.getMonth()+1).padStart(2,"0")}/${String(d.getDate()).padStart(2,"0")}`; const ACTIVE_MINUTES_DISPLAY = END_HOUR > START_HOUR ? (END_HOUR - START_HOUR) * 60 // same-day window : ((24 - START_HOUR) + END_HOUR) * 60; const SLOTS_PER_DAY = Math.ceil(ACTIVE_MINUTES_DISPLAY / CHUNK_MINUTES); function dayKeyAndOffset(t) { const d = new Date(t); const h = d.getHours(); const dayRow = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (h < START_HOUR ? 1 : 0)); let offsetMin; if (h >= START_HOUR) offsetMin = (h - START_HOUR) * 60 + d.getMinutes(); else offsetMin = (24 - START_HOUR) * 60 + h * 60 + d.getMinutes(); return { keyDate: dayRow, mmdd: mmdd(dayRow), offsetMin }; } async function fetchBucket(bucket) { const url = `/api/v1/drafts/${draftId}/versions` + (bucket ? `?bucket=${bucket}` : ""); const r = await fetch(url, { credentials: "include" }); return r.json(); } const allVersions = []; let bucket = undefined; do { const payload = await fetchBucket(bucket); if (payload?.versions?.contents?.length) allVersions.push(...payload.versions.contents); bucket = FOLLOW_PREVIOUS ? payload?.versions?.previousBucket : null; } while (bucket); // === Build minute and chunk sets (GLOBAL, all-day, window-agnostic) === const minuteEpochsAll = new Set( allVersions.map(v => new Date(v.LastModified)).map(d => Math.floor(d.getTime() / 60000)) ); const chunkEpochsAll = new Set( [...minuteEpochsAll].map(min => Math.floor(min / CHUNK_MINUTES)) // unique chunks across 24h ); // === Build DISPLAY rows (windowed) === const dayRows = new Map(); // key -> {dateObj,label,slots[]} for (const minEpoch of minuteEpochsAll) { const d = new Date(minEpoch * 60000); const h = d.getHours(); const inWindow = (h >= START_HOUR) || (h < END_HOUR); if (!inWindow) continue; // only affects visuals, not totals const { keyDate, mmdd: label, offsetMin } = dayKeyAndOffset(d); const key = keyDate.getTime(); if (!dayRows.has(key)) { dayRows.set(key, { dateObj: keyDate, label, slots: Array(SLOTS_PER_DAY).fill(INACTIVE_CHAR) }); } const row = dayRows.get(key); const slot = Math.floor(offsetMin / CHUNK_MINUTES); if (slot >= 0 && slot < SLOTS_PER_DAY) row.slots[slot] = ACTIVE_CHAR; } const rowsSorted = [...dayRows.values()].sort((a,b) => a.dateObj - b.dateObj); console.log( `Draft ${draftId} — ${CHUNK_MINUTES}-min chunks, window ${START_HOUR}:00→${END_HOUR}:00 (display only)` ); console.log(`${ACTIVE_CHAR}=writing, ${INACTIVE_CHAR}=idle`); for (const r of rowsSorted) { console.log(`${r.label} ${r.slots.join("")}`); } // === Window-independent total active time === const totalMinutes = chunkEpochsAll.size * CHUNK_MINUTES; const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; console.log(`\nTotal active writing time (24h, window-agnostic, chunked): ${hours}h ${minutes}m`); })();