// Admin / Maintenance page: health, storage stats, backup browser, stats, manual actions.
// ---- Range-aware stats engine -----------------------------------------------
// Generates deterministic per-bucket counts based on seed numbers so switching
// range feels real (each range animates between fixed shapes, not random noise).
const RANGE_OPTIONS = [
{ key: "week", label: "This week", buckets: 7, bucketLabel: "d", granularity: "day" },
{ key: "month", label: "This month", buckets: 5, bucketLabel: "w", granularity: "week" },
{ key: "year", label: "This year", buckets: 12, bucketLabel: "m", granularity: "month" },
{ key: "7d", label: "Last 7d", buckets: 7, bucketLabel: "d", granularity: "day" },
{ key: "30d", label: "Last 30d", buckets: 30, bucketLabel: "d", granularity: "day" },
{ key: "365d", label: "Last 365d", buckets: 12, bucketLabel: "m", granularity: "month" },
];
// Returns [{start, end}] bucket boundaries matching bucketLabelsFor(rangeKey)
function bucketBounds(rangeKey, refDate) {
const d = new Date(refDate);
if (rangeKey === 'week') {
const dow = (d.getDay() + 6) % 7;
const mon = new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow);
return Array.from({length: 7}, (_, i) => {
const s = new Date(mon); s.setDate(mon.getDate() + i);
const e = new Date(s); e.setDate(s.getDate() + 1);
return { start: s, end: e };
});
}
if (rangeKey === '7d') {
return Array.from({length: 7}, (_, i) => {
const offset = 6 - i;
const s = new Date(d.getFullYear(), d.getMonth(), d.getDate() - offset);
const e = new Date(s); e.setDate(s.getDate() + 1);
return { start: s, end: e };
});
}
if (rangeKey === '30d') {
return Array.from({length: 30}, (_, i) => {
const offset = 29 - i;
const s = new Date(d.getFullYear(), d.getMonth(), d.getDate() - offset);
const e = new Date(s); e.setDate(s.getDate() + 1);
return { start: s, end: e };
});
}
if (rangeKey === 'month') {
const y = d.getFullYear(), mo = d.getMonth();
return Array.from({length: 5}, (_, i) => ({
start: new Date(y, mo, 1 + i * 7),
end: i < 4 ? new Date(y, mo, 1 + (i + 1) * 7) : new Date(y, mo + 1, 1),
}));
}
if (rangeKey === 'year') {
const y = d.getFullYear();
return Array.from({length: 12}, (_, i) => ({
start: new Date(y, i, 1),
end: new Date(y, i + 1, 1),
}));
}
if (rangeKey === '365d') {
return Array.from({length: 12}, (_, i) => {
const offset = 11 - i;
const y = d.getFullYear(), mo = d.getMonth();
return {
start: new Date(y, mo - offset, 1),
end: new Date(y, mo - offset + 1, 1),
};
});
}
return [];
}
function countInBuckets(history, buckets) {
const keys = ['created', 'in-progress', 'blocked', 'postponed', 'done', 'cancelled'];
const out = Object.fromEntries(keys.map(k => [k, Array(buckets.length).fill(0)]));
for (const h of history) {
const ts = new Date(h.timestamp);
const idx = buckets.findIndex(b => ts >= b.start && ts < b.end);
if (idx < 0) continue;
if (h.action === 'item_created') {
out.created[idx]++;
} else if (h.action === 'status_changed') {
const m = h.details.match(/→\s*(\S[\s\S]*?)$/);
if (m) {
const ns = m[1].trim();
if (ns in out) out[ns][idx]++;
}
}
}
return out;
}
function bucketLabelsFor(rangeKey) {
const today = new Date();
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const days = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
if (rangeKey === "week") {
return days;
}
if (rangeKey === "7d") {
const out = [];
for (let i = 6; i >= 0; i--) {
const d = new Date(today); d.setDate(today.getDate() - i);
out.push(days[(d.getDay() + 6) % 7]);
}
return out;
}
if (rangeKey === "30d") {
const out = [];
for (let i = 29; i >= 0; i--) {
const d = new Date(today); d.setDate(today.getDate() - i);
out.push(String(d.getDate()));
}
return out;
}
if (rangeKey === "month") {
return ["W1","W2","W3","W4","W5"];
}
if (rangeKey === "year") {
return months;
}
if (rangeKey === "365d") {
const out = [];
for (let i = 11; i >= 0; i--) {
const d = new Date(today); d.setMonth(today.getMonth() - i);
out.push(months[d.getMonth()]);
}
return out;
}
return [];
}
// Status keys in the order we want them stacked (bottom → top).
// "created" is a synthetic bucket: items that entered the system in that period.
const CHART_STATUSES = [
{ key: "created", label: "Created", color: "oklch(0.78 0.13 220)" }, // sky cyan-blue
{ key: "in-progress", label: "In progress", color: "oklch(0.72 0.14 270)" }, // lavender
{ key: "blocked", label: "Blocked", color: "oklch(0.68 0.20 28)" }, // red — fixed (0° HSL)
{ key: "postponed", label: "Postponed", color: "oklch(0.83 0.16 90)" }, // bright golden yellow — bisector of red↔green (≈70° HSL)
{ key: "done", label: "Done", color: "oklch(0.68 0.16 150)" }, // green — fixed (140° HSL)
{ key: "cancelled", label: "Cancelled", color: "oklch(0.62 0.04 280)" }, // slate
];
function computeRangeStats(rangeKey, dataStats, history) {
const opt = RANGE_OPTIONS.find(r => r.key === rangeKey);
const labels = bucketLabelsFor(rangeKey);
const now = new Date();
// Prior-period reference: shift back by one equivalent period
const priorRef = new Date(now);
if (rangeKey === 'week' || rangeKey === '7d') priorRef.setDate(priorRef.getDate() - 7);
else if (rangeKey === 'month' || rangeKey === '30d') priorRef.setMonth(priorRef.getMonth() - 1);
else if (rangeKey === 'year' || rangeKey === '365d') priorRef.setFullYear(priorRef.getFullYear() - 1);
const curr = countInBuckets(history, bucketBounds(rangeKey, now));
const prior = countInBuckets(history, bucketBounds(rangeKey, priorRef));
const series = {
created: curr.created,
'in-progress': curr['in-progress'],
blocked: curr.blocked,
postponed: curr.postponed,
done: curr.done,
cancelled: curr.cancelled,
};
const sum = arr => arr.reduce((a, b) => a + b, 0);
const totalCreated = sum(curr.created);
const totalCompleted = sum(curr.done);
const priorCreated = sum(prior.created);
const priorCompleted = sum(prior.done);
const pct = (c, p) => p === 0 ? (c > 0 ? 100 : 0) : Math.round(((c - p) / p) * 100);
const periodWeeks = { week: 1, '7d': 1, month: 4.3, '30d': 4.3, year: 52, '365d': 52 }[rangeKey];
const throughput = +(totalCompleted / periodWeeks).toFixed(1);
const priorThroughput = +(priorCompleted / periodWeeks).toFixed(1);
const completionRate = totalCreated > 0 ? Math.round((totalCompleted / totalCreated) * 100) : 0;
const priorCompletionRate = priorCreated > 0 ? Math.round((priorCompleted / priorCreated) * 100) : 0;
const cycle = dataStats.avgInProgressDays ?? 0;
return {
opt,
labels,
series,
totals: { created: totalCreated, completed: totalCompleted },
kpis: {
created: { value: totalCreated, delta: pct(totalCreated, priorCreated) },
completed: { value: totalCompleted, delta: pct(totalCompleted, priorCompleted) },
throughput: { value: throughput, delta: pct(throughput, priorThroughput), unit: '/wk' },
cycle: { value: cycle, delta: 0, unit: 'd', deltaInverse: true },
rate: { value: completionRate, delta: completionRate - priorCompletionRate, unit: '%' },
},
};
}
function StatsOverview({ dataStats, mostActiveProject, history }) {
const [range, setRange] = useState("week");
const [chartMode, setChartMode] = useState("cumulative"); // cumulative | breakdown
const [hiddenStatuses, setHiddenStatuses] = useState(new Set());
const stats = useMemo(() => computeRangeStats(range, dataStats, history), [range, dataStats, history]);
const visibleStatuses = CHART_STATUSES.filter(s => !hiddenStatuses.has(s.key));
// Both modes show stacked totals; max is bucket sum across visible series.
const maxBar = useMemo(() => {
let m = 1;
for (let i = 0; i < stats.labels.length; i++) {
const s = visibleStatuses.reduce((acc, st) => acc + stats.series[st.key][i], 0);
if (s > m) m = s;
}
return m;
}, [stats, visibleStatuses]);
const dense = stats.labels.length > 12;
const labelStride = dense ? Math.ceil(stats.labels.length / 8) : 1;
const toggleStatus = (key) => {
setHiddenStatuses(prev => {
const next = new Set(prev);
if (next.has(key)) next.delete(key); else next.add(key);
return next;
});
};
return (
Stats overview
{stats.opt.label.toLowerCase()} · most active in {mostActiveProject}
{RANGE_OPTIONS.map(opt => (
))}
{chartMode === "breakdown" ? (
CHART_STATUSES.map(s => {
const off = hiddenStatuses.has(s.key);
return (
);
})
) : (
Total activity
)}
{stats.labels.map((lbl, i) => {
const showLabel = i % labelStride === 0;
const bucketTotal = visibleStatuses.reduce((acc, st) => acc + stats.series[st.key][i], 0);
const tip = chartMode === "breakdown"
? visibleStatuses.map(st => `${st.label} ${stats.series[st.key][i]}`).join(" · ")
: `total ${bucketTotal}`;
// Pre-compute % heights and bottoms for breakdown so segments stack
// without flex rounding errors. Bottom = sum of segments below.
let acc = 0;
const segs = chartMode === "breakdown"
? visibleStatuses.map(st => {
const v = stats.series[st.key][i];
if (v === 0) return null;
const heightPct = (v / maxBar) * 100;
const bottomPct = (acc / maxBar) * 100;
acc += v;
return { st, v, heightPct, bottomPct };
}).filter(Boolean)
: [];
return (
{chartMode === "breakdown" ? (
{segs.map((s, idx) => (
))}
) : (
)}
{showLabel ? lbl : ""}
{showLabel && bucketTotal > 0 && (
{bucketTotal}
)}
);
})}
);
}
const ModeIconCumulative = () => (
);
const ModeIconBreakdown = () => (
);
function Kpi({ label, value, delta, unit, accent, deltaInverse }) {
// For metrics where lower is better (cycle time), invert sign
const positive = deltaInverse ? delta < 0 : delta > 0;
const negative = deltaInverse ? delta > 0 : delta < 0;
const arrow = delta === 0 ? "→" : positive ? "▲" : "▼";
const cls = delta === 0 ? "flat" : positive ? "up" : "down";
const formatted = Math.abs(delta) === 0 ? "0" : `${Math.abs(delta)}${unit === "%" ? "pp" : "%"}`;
return (
{label}
{value}{unit && {unit}}
{arrow} {formatted} vs prior
);
}
function AdminPage({ data, onClose, history, onForceSave, onForceBackup, onCompact, onRestore, onDownloadBackup }) {
const [previewBackup, setPreviewBackup] = useState(null);
const counts = countByStatus(data.entries);
const total = countAll(data.entries);
const maxStat = Math.max(1, ...Object.values(data.stats.statusMix));
return (
Admin · Maintenance
System overview
{/* Health */}
System health
{data.health.integrityOk ? "✓ integrity ok" : "checksum mismatch"}
- Last save
- {fmtTimestamp(data.health.lastSave)}
- Last backup
- {fmtTimestamp(data.health.lastBackup)}
- Storage mode
- {data.health.mode}
{data.health.masterPath && (
- File path
- {data.health.masterPath}
)}
- Checksum
- {data.meta.checksum.slice(0, 32)}…
- Saved at
- {data.meta.saved}
- Entries / history
- {data.meta.entryCount} · {data.meta.historyCount}
{/* Storage */}
Storage
{fmtBytes(data.health.masterSize + data.health.backupDirSize + data.health.statsSize + data.health.historySize)} total
- backlog.md
- {fmtBytes(data.health.masterSize)} · {data.meta.entryCount} entries
{data.health.masterPath && (
- Path
- {data.health.masterPath}
)}
- history.jsonl
- {fmtBytes(data.health.historySize)} · {data.meta.historyCount.toLocaleString()} events
- backups/
- {fmtBytes(data.health.backupDirSize)} · {data.health.backupCount} files
{data.health.backupsPath && (
- Backups
- {data.health.backupsPath}
)}
- stats.jsonl
- {fmtBytes(data.health.statsSize)} · 90-day rollup
- Oldest event
- {fmtTimestamp(data.health.historyOldest)}
{/* Manual actions */}
Manual actions
{/* Stats overview */}
{/* Status mix */}
Status mix
{STATUSES.map(s => {
const v = data.stats.statusMix[s.key] || 0;
return (
{s.glyph}
{s.label}
{v}
);
})}
{/* Backup browser */}
Backup browser
{data.backups.length} files · keeping ≥7 days, ≥30 days of dailies
| Filename |
Timestamp |
Size |
Entries |
Integrity |
|
{data.backups.map(b => (
| {b.name} |
{fmtTimestamp(b.timestamp)} |
{fmtBytes(b.size)} |
{b.entries ?? —} |
{b.valid
? ✓ valid
: mismatch}
|
|
))}
{previewBackup && (
{previewBackup}
read-only preview
{`# Backlog
## [P0] 🔥 Personal backlog app
- [x] Markdown parser + integrity marker *(done: 2026-04-29)*
- [x] Tokenize three sections
- [x] SHA-256 over entries+history
- [/] Tree renderer with parent-visibility filter *(due: 2026-05-04)*
- [ ] Expand / collapse animations
- [ ] Drag handle within priority group *(due: 2026-05-06)*
- [!] File System Access API direct mode *(reason: Need to test on Edge — no machine yet)*
…
| Timestamp | Item ID | Action | Details |
…
`}
)}
{/* Recent history */}
Audit log
{data.meta.historyCount.toLocaleString()} events ·
{" "}{fmtBytes(data.health.historySize)} ·
{" "}since {fmtShortDate(data.health.historyOldest)} ·
{" "}showing latest 10
| Timestamp |
Item ID |
Action |
Details |
{history.slice(0, 10).map((h, i) => (
| {h.timestamp} |
{h.itemId} |
{h.action.replace(/_/g, " ")} |
{h.details} |
))}
);
}
window.AdminPage = AdminPage;