'use client'; import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Loader2, AlertCircle, BarChart2, ChevronDown, ChevronRight } from 'lucide-react'; import Link from 'next/link'; import { fetchQuotaHistory, type QuotaLogEntry } from '@/lib/api'; import f from '@/components/shared/FormField.module.css'; import styles from './page.module.css'; const OPERATION_COST: Record = { 'videos.update': '50', 'playlistItems.insert': '50', 'playlistItems.delete': '50', 'videos.list': '1', 'playlistItems.list': '1', 'playlists.list': '1', 'channels.list': '1', }; function relativeTime(iso: string) { const diff = Date.now() - new Date(iso).getTime(); const s = Math.floor(diff / 1000); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; } function formatDateTime(iso: string) { return new Date(iso).toLocaleString(); } function operationLabel(op: string) { const labels: Record = { 'videos.update': 'Push to YouTube', 'videos.list': 'Fetch video metadata', 'playlistItems.list': 'List playlist items', 'playlistItems.insert': 'Add to playlist', 'playlistItems.delete': 'Remove from playlist', 'playlists.list': 'List playlists', 'channels.list': 'Fetch channel info', }; return labels[op] ?? op; } const DAYS_OPTIONS = [ { value: 1, label: 'Today' }, { value: 7, label: 'Last 7 days' }, { value: 30, label: 'Last 30 days' }, { value: 90, label: 'Last 90 days' }, ]; function groupByOperation(items: QuotaLogEntry[]) { const map = new Map(); for (const item of items) { const existing = map.get(item.operation) ?? { count: 0, units: 0 }; map.set(item.operation, { count: existing.count + 1, units: existing.units + item.units }); } return Array.from(map.entries()) .map(([op, stats]) => ({ op, ...stats })) .sort((a, b) => b.units - a.units); } // ── Action group clustering ────────────────────────────────────────────────── const ACTION_TYPE_LABELS: Record = { channel_import: 'Channel Import', video_sync: 'Push to YouTube', video_refresh: 'Refresh from YouTube', playlist_sync: 'Playlist Sync', playlist_add: 'Add to Playlist', playlist_remove: 'Remove from Playlist', }; interface ActionGroup { actionId: string | null; channelId: string | null; channelName: string | null; startTs: number; totalUnits: number; entries: QuotaLogEntry[]; label: string; detail: string | null; videoId: string | null; youtubeVideoId: string | null; } function buildLabel( actionType: string | null, entries: QuotaLogEntry[], ): { label: string; detail: string | null; videoId: string | null; youtubeVideoId: string | null } { const label = (actionType && ACTION_TYPE_LABELS[actionType]) ?? operationLabel(entries[0]?.operation ?? ''); if (actionType === 'video_sync' || actionType === 'video_refresh') { const e = entries.find((e) => e.videoTitle ?? e.videoId); return { label, detail: e?.videoTitle ?? e?.videoId ?? null, videoId: e?.videoId ?? null, youtubeVideoId: e?.youtubeVideoId ?? null, }; } if (actionType === 'playlist_add' || actionType === 'playlist_remove') { const e = entries.find((e) => e.entityLabel); return { label, detail: e?.entityLabel ?? null, videoId: null, youtubeVideoId: null }; } return { label, detail: null, videoId: null, youtubeVideoId: null }; } function groupByActionId(items: QuotaLogEntry[]): ActionGroup[] { if (!items.length) return []; const map = new Map(); for (const entry of items) { const key = entry.actionId ?? `ungrouped:${entry.id}`; const ts = new Date(entry.createdAt).getTime(); if (!map.has(key)) { map.set(key, { actionId: entry.actionId, channelId: entry.channelId, channelName: entry.channelName, startTs: ts, totalUnits: 0, entries: [], label: '', detail: null, videoId: null, youtubeVideoId: null, }); } const group = map.get(key)!; group.totalUnits += entry.units; group.entries.push(entry); if (ts < group.startTs) group.startTs = ts; } const groups = Array.from(map.values()); for (const group of groups) { const actionType = group.entries[0]?.actionType ?? null; const { label, detail, videoId, youtubeVideoId } = buildLabel(actionType, group.entries); group.label = label; group.detail = detail; group.videoId = videoId; group.youtubeVideoId = youtubeVideoId; group.entries.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); } const sorted = groups.sort((a, b) => b.startTs - a.startTs); return clusterVideoGroups(sorted); } // Merge action groups that share the same videoId and are within 60 seconds of each other. // This handles the common case where a video push and playlist changes are logged as separate // actionIds but belong to the same user-initiated save action. function clusterVideoGroups(groups: ActionGroup[]): ActionGroup[] { const result: ActionGroup[] = []; const used = new Set(); for (let i = 0; i < groups.length; i++) { if (used.has(i)) continue; const g = groups[i]; if (!g.videoId) { result.push(g); continue; } const toMerge: ActionGroup[] = [g]; for (let j = i + 1; j < groups.length; j++) { if (used.has(j)) continue; const other = groups[j]; if (other.videoId !== g.videoId) continue; // groups are sorted newest-first so g.startTs >= other.startTs if (g.startTs - other.startTs > 60_000) continue; toMerge.push(other); used.add(j); } if (toMerge.length === 1) { result.push(g); } else { const allEntries = toMerge .flatMap((m) => m.entries) .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); const totalUnits = toMerge.reduce((s, m) => s + m.totalUnits, 0); const startTs = Math.min(...toMerge.map((m) => m.startTs)); // Prefer the video_sync group's label if present; otherwise keep the first const primary = toMerge.find((m) => m.entries.some((e) => e.actionType === 'video_sync')) ?? toMerge[0]; result.push({ ...primary, entries: allEntries, totalUnits, startTs }); } } return result; } // ── Components ─────────────────────────────────────────────────────────────── function ActionGroupRow({ group }: { group: ActionGroup }) { const [open, setOpen] = useState(false); const startIso = new Date(group.startTs).toISOString(); return (
{open && (
{group.entries.map((entry) => ( ))}
Time Operation Detail Units
{relativeTime(entry.createdAt)} {operationLabel(entry.operation)} {entry.operation} {entry.videoId ? ( {entry.videoTitle ?? entry.videoId} {entry.youtubeVideoId && ( {entry.youtubeVideoId} )} ) : entry.entityLabel ? ( {entry.entityLabel} ) : ( )} = 50 ? styles.unitsHigh : ''}`}> {entry.units}
)}
); } // ── Page ───────────────────────────────────────────────────────────────────── export default function QuotaHistoryPage() { const [days, setDays] = useState(7); const { data, isLoading, isError } = useQuery({ queryKey: ['quotaHistory', days], queryFn: () => fetchQuotaHistory(days), refetchInterval: 60_000, }); const summary = data ? groupByOperation(data.items) : []; const groups = data ? groupByActionId(data.items) : []; const limit = 9_000; const pct = data ? Math.round((data.totalUnits / limit) * 100) : 0; return (
Logging

Quota History

{/* Summary cards */} {data && (
{data.totalUnits.toLocaleString()}
Total units used
70 ? styles.quotaFillWarn : ''} ${pct > 90 ? styles.quotaFillError : ''}`} style={{ width: `${Math.min(pct, 100)}%` }} />
{pct}% of 9,000 daily limit
{groups.length.toLocaleString()}
Actions
{data.items.length} API calls total
{summary[0] && (
{summary[0].units.toLocaleString()}
Highest cost: {operationLabel(summary[0].op)}
{summary[0].count} calls × {OPERATION_COST[summary[0].op] ?? '?'} units
)}
)} {/* Breakdown by operation */} {summary.length > 0 && (

Usage by operation

{summary.map(({ op, count, units }) => ( ))}
Operation Calls Units each Total units
{operationLabel(op)} {op} {count.toLocaleString()} {OPERATION_COST[op] ?? '?'} {units.toLocaleString()}
)} {/* Action groups */}

Actions {data && {groups.length}}

{isLoading && (
Loading quota history…
)} {isError && (
Failed to load quota history.
)} {data?.items.length === 0 && (
No quota usage recorded for this period.
)} {groups.length > 0 && (
{groups.map((group, i) => ( ))}
)}
); }