Skip to content

Instantly share code, notes, and snippets.

@Lucent
Last active November 18, 2025 09:27
Show Gist options
  • Select an option

  • Save Lucent/505dc4c0a40a7b45c7a70e47d643417f to your computer and use it in GitHub Desktop.

Select an option

Save Lucent/505dc4c0a40a7b45c7a70e47d643417f to your computer and use it in GitHub Desktop.
Fetches Substack draft save times and bins them into configurable N-minute chunks, and prints one ASCII line per day showing active vs idle periods
// Substack versions → per-day ASCII timeline + multi-scale totals +
// fractal dimension (D) + 5s-resolution "true" writing time estimate.
// Paste into console on a /publish/post/{id} page.
(async () => {
const CHUNK_MINUTES = 5; // bucket size for DISPLAY + legacy total
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
// Bin sizes for fractal analysis (seconds)
const FRACTAL_BIN_SECONDS = [30, 60, 120, 240, 480, 960, 1920];
// "Micro" resolution, based on observed Substack save cadence (2–10s typical)
const MICRO_BIN_SECONDS = 5;
const m = location.pathname.match(/\/publish\/post\/(\d+)/);
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
: ((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;
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);
// === Raw timestamps (ms since epoch), one per version ===
const timestampsMs = allVersions
.map(v => new Date(v.LastModified).getTime())
.sort((a, b) => a - b);
// === Minute & chunk sets for DISPLAY + legacy total (GLOBAL, 24h) ===
const minuteEpochsAll = new Set(
timestampsMs.map(t => Math.floor(t / 60000))
);
const chunkEpochsAll = new Set(
[...minuteEpochsAll].map(min => Math.floor(min / CHUNK_MINUTES))
);
// === 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;
const { keyDate, mmdd: label, offsetMin } = dayKeyAndOffset(d.getTime());
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("")}`);
}
// === Legacy window-independent total active time at 5-min granularity ===
const legacyTotalMinutes = chunkEpochsAll.size * CHUNK_MINUTES;
const legacyHours = Math.floor(legacyTotalMinutes / 60);
const legacyMinutes = legacyTotalMinutes % 60;
console.log(
`\nTotal active writing time (24h, window-agnostic, ${CHUNK_MINUTES}-min chunks): ${legacyHours}h ${legacyMinutes}m`
);
// === Multi-scale totals for FRACTAL_BIN_SECONDS (24h, window-agnostic) ===
function totalsForBinSeconds(binSec) {
const binMs = binSec * 1000;
const bins = new Set();
for (const t of timestampsMs) {
bins.add(Math.floor(t / binMs));
}
const binCount = bins.size;
const totalMinutes = (binCount * binSec) / 60;
const hours = Math.floor(totalMinutes / 60);
const minutes = Math.round(totalMinutes % 60);
return { binSec, binCount, totalMinutes, hours, minutes };
}
const binResults = FRACTAL_BIN_SECONDS.map(totalsForBinSeconds);
console.log(`\nTotal active writing times by bin (24h, window-agnostic):`);
for (const r of binResults) {
const label = r.binSec >= 60 ? `${r.binSec / 60}m` : `${r.binSec}s`;
console.log(` ${label}: ${r.hours}h ${String(r.minutes).padStart(2, "0")}m`);
}
// === Fractal dimension D from N(b) ~ b^(−D) ===
const xs = binResults.map(r => Math.log(r.binSec)); // log b
const ys = binResults.map(r => Math.log(r.binCount)); // log N(b)
const n = xs.length;
let sumX = 0, sumY = 0, sumXX = 0, sumXY = 0;
for (let i = 0; i < n; i++) {
const x = xs[i];
const y = ys[i];
sumX += x;
sumY += y;
sumXX += x * x;
sumXY += x * y;
}
const denom = n * sumXX - sumX * sumX;
const slope = (n * sumXY - sumX * sumY) / denom; // slope of log N vs log b
const D = -slope; // N(b) ∝ b^(−D)
// === Fractal "true time" at MICRO_BIN_SECONDS resolution ===
// Using N(ε0) ≈ N(b)·(b/ε0)^D, T_true ≈ N(ε0)·ε0 = N(b)·b^D·ε0^(1−D)
let sumTrueSec = 0;
for (const r of binResults) {
const B = r.binSec;
const N_B = r.binCount;
const trueSec = N_B * Math.pow(B, D) * Math.pow(MICRO_BIN_SECONDS, 1 - D);
sumTrueSec += trueSec;
}
const avgTrueSec = sumTrueSec / binResults.length;
const trueTotalMinutes = avgTrueSec / 60;
const trueHours = Math.floor(trueTotalMinutes / 60);
const trueMinutes = Math.round(trueTotalMinutes % 60);
console.log(`\nFractal-ish start/stoppiness:`);
console.log(` Dimension D ≈ ${D.toFixed(3)}`);
console.log(
` Estimated "true" writing time at ~${MICRO_BIN_SECONDS}s resolution: ≈ ${trueHours}h ${String(trueMinutes).padStart(2, "0")}m`
);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment