From 8dbce6f55b8e46b59ba6b369748d82ca2d1e416d Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 00:45:38 +0800 Subject: [PATCH 01/11] feat: add changelog page with neon sidebar link and modern timeline UI - Add docs/changelog.md with v1.0.0 and v1.0.1 entries - Add changelog link to sidebar (desktop + mobile) with neon glow on icon and text - Remove 'new' badges from Feedback and Changelog sidebar links - Add neon-pulse animation keyframe to tailwind config - Create /dashboard/changelog page with structured markdown parser - Modern timeline layout with version cards, colored category badges, and staggered fade-in animations --- docs/changelog.md | 33 +++ .../src/app/dashboard/changelog/client.tsx | 222 ++++++++++++++++++ .../src/app/dashboard/changelog/page.tsx | 103 ++++++++ .../src/components/dashboard/Sidebar.tsx | 65 ++++- packages/kal-frontend/tailwind.config.ts | 11 + 5 files changed, 423 insertions(+), 11 deletions(-) create mode 100644 docs/changelog.md create mode 100644 packages/kal-frontend/src/app/dashboard/changelog/client.tsx create mode 100644 packages/kal-frontend/src/app/dashboard/changelog/page.tsx diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..c404cb4 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to Kal will be documented in this file. + +--- + +## [1.0.1] - 2026-04-06 + +### Fixed + +- Rate limit usage stats now only count successful requests — previously, rate-limited (429) responses were still counted against your usage, which made your dashboard stats appear higher than actual usage +- Improved error tracking in request logs with clearer error messages for failed requests + +--- + +## [1.0.0] - 2026-03-17 + +### Added + +- API v1 with versioned endpoints (`/api/v1/*`) +- Food database with comprehensive nutritional data +- API key management dashboard +- Request logging and analytics +- Setup wizard with code examples +- Feedback and bug reporting system +- Collapsible sidebar with mobile-responsive drawer + +### Changed + +- Migrated all API endpoints to versioned paths +- Improved dashboard performance and loading states + +--- diff --git a/packages/kal-frontend/src/app/dashboard/changelog/client.tsx b/packages/kal-frontend/src/app/dashboard/changelog/client.tsx new file mode 100644 index 0000000..1430b5c --- /dev/null +++ b/packages/kal-frontend/src/app/dashboard/changelog/client.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { FileText, Gift, Tool, Zap } from "react-feather"; + +import type { ChangelogEntry } from "./page"; + +const CATEGORY_CONFIG: Record< + string, + { icon: typeof Gift; color: string; bg: string; border: string; glow: string } +> = { + Added: { + icon: Gift, + color: "text-emerald-400", + bg: "bg-emerald-400/10", + border: "border-emerald-400/20", + glow: "shadow-[0_0_8px_rgba(52,211,153,0.15)]", + }, + Fixed: { + icon: Tool, + color: "text-amber-400", + bg: "bg-amber-400/10", + border: "border-amber-400/20", + glow: "shadow-[0_0_8px_rgba(251,191,36,0.15)]", + }, + Changed: { + icon: Zap, + color: "text-sky-400", + bg: "bg-sky-400/10", + border: "border-sky-400/20", + glow: "shadow-[0_0_8px_rgba(56,189,248,0.15)]", + }, +}; + +const DEFAULT_CATEGORY = { + icon: Zap, + color: "text-purple-400", + bg: "bg-purple-400/10", + border: "border-purple-400/20", + glow: "shadow-[0_0_8px_rgba(192,132,252,0.15)]", +}; + +function formatDate(dateStr: string): string { + const date = new Date(dateStr + "T00:00:00"); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +function VersionCard({ + entry, + isLatest, + index, +}: { + entry: ChangelogEntry; + isLatest: boolean; + index: number; +}) { + return ( +
+ {/* Timeline dot */} +
+
+
+ + {/* Card */} +
+ {/* Version header */} +
+ + v{entry.version} + + {isLatest && ( + + latest + + )} + + {formatDate(entry.date)} + +
+ + {/* Categories */} +
+ {entry.categories.map((category) => { + const config = CATEGORY_CONFIG[category.name] ?? DEFAULT_CATEGORY; + const Icon = config.icon; + + return ( +
+ {/* Category badge */} +
+ + {category.name} +
+ + {/* Items */} +
    + {category.items.map((item, i) => ( +
  • + + + + +
  • + ))} +
+
+ ); + })} +
+
+
+ ); +} + +/** Renders inline `code` segments with accent styling */ +function HighlightCode({ text }: { text: string }) { + const parts = text.split(/(`[^`]+`)/g); + return ( + <> + {parts.map((part, i) => + part.startsWith("`") && part.endsWith("`") ? ( + + {part.slice(1, -1)} + + ) : ( + {part} + ) + )} + + ); +} + +interface ChangelogClientProps { + entries: ChangelogEntry[]; +} + +export default function ChangelogClient({ entries }: ChangelogClientProps) { + return ( +
+ {/* Header */} +
+
+ +

+ Changelog +

+
+

+ All the latest updates and improvements to Kal +

+
+ + {entries.length === 0 ? ( +
+ +

+ No changelog entries yet. Check back soon. +

+
+ ) : ( + /* Timeline */ +
+ {/* Vertical timeline line */} +
+ + {/* Entries */} +
+ {entries.map((entry, index) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/packages/kal-frontend/src/app/dashboard/changelog/page.tsx b/packages/kal-frontend/src/app/dashboard/changelog/page.tsx new file mode 100644 index 0000000..6ac63e4 --- /dev/null +++ b/packages/kal-frontend/src/app/dashboard/changelog/page.tsx @@ -0,0 +1,103 @@ +import fs from "fs"; +import path from "path"; + +import { getLogtoContext } from "@logto/next/server-actions"; +import { redirect } from "next/navigation"; + +import ChangelogClient from "./client"; + +import { getLogtoConfig } from "@/lib/logto"; + +export const metadata = { + title: "Changelog - Kal Dashboard", + description: "See what's new in Kal", +}; + +export interface ChangelogEntry { + version: string; + date: string; + categories: { + name: string; + items: string[]; + }[]; +} + +/** + * Parses a Keep-a-Changelog-style markdown string into structured data. + * + * Expected format per release: + * ## [version] - YYYY-MM-DD + * ### Category + * - item + */ +function parseChangelog(raw: string): ChangelogEntry[] { + const entries: ChangelogEntry[] = []; + let current: ChangelogEntry | null = null; + let currentCategory: { name: string; items: string[] } | null = null; + + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + + // Match version heading: ## [1.0.1] - 2026-04-06 + const versionMatch = trimmed.match( + /^##\s+\[([^\]]+)\]\s*-\s*(\d{4}-\d{2}-\d{2})/ + ); + if (versionMatch) { + if (current) { + if (currentCategory) current.categories.push(currentCategory); + entries.push(current); + } + current = { + version: versionMatch[1], + date: versionMatch[2], + categories: [], + }; + currentCategory = null; + continue; + } + + // Match category heading: ### Added / ### Fixed / ### Changed + const categoryMatch = trimmed.match(/^###\s+(.+)/); + if (categoryMatch && current) { + if (currentCategory) current.categories.push(currentCategory); + currentCategory = { name: categoryMatch[1], items: [] }; + continue; + } + + // Match list item: - Some change description + const itemMatch = trimmed.match(/^-\s+(.+)/); + if (itemMatch && currentCategory) { + currentCategory.items.push(itemMatch[1]); + } + } + + // Push last pending entries + if (current) { + if (currentCategory) current.categories.push(currentCategory); + entries.push(current); + } + + return entries; +} + +export default async function ChangelogPage() { + const config = getLogtoConfig(); + const { isAuthenticated } = await getLogtoContext(config); + + if (!isAuthenticated) { + redirect("/"); + } + + const changelogPath = path.resolve(process.cwd(), "../../docs/changelog.md"); + let raw = ""; + + try { + raw = fs.readFileSync(changelogPath, "utf-8"); + } catch { + raw = ""; + } + + const entries = parseChangelog(raw); + + return ; +} diff --git a/packages/kal-frontend/src/components/dashboard/Sidebar.tsx b/packages/kal-frontend/src/components/dashboard/Sidebar.tsx index b7568b9..2fa5269 100644 --- a/packages/kal-frontend/src/components/dashboard/Sidebar.tsx +++ b/packages/kal-frontend/src/components/dashboard/Sidebar.tsx @@ -10,6 +10,7 @@ import { ChevronRight, Code, Database, + FileText, Home, Key, List, @@ -175,6 +176,30 @@ export function Sidebar({ onSignOut }: SidebarProps) { })} + {/* Changelog */} +
+ setMobileOpen(false)} + className={` + flex items-center gap-3 px-4 py-3 mx-2 rounded-lg + ${ + pathname === "/dashboard/changelog" + ? "bg-accent/10 text-accent border border-accent/30" + : "text-content-secondary hover:bg-dark-elevated hover:text-content-primary" + } + `} + > + + + Changelog + + +
+ {/* Feedback */}
Review & Bug - - new -
@@ -270,6 +292,32 @@ export function Sidebar({ onSignOut }: SidebarProps) { })} + {/* Changelog */} +
+ + + {!collapsed && ( + + Changelog + + )} + +
+ {/* Feedback */}
{!collapsed && ( - <> - - Review & Bug - - - new - - + + Review & Bug + )}
diff --git a/packages/kal-frontend/tailwind.config.ts b/packages/kal-frontend/tailwind.config.ts index a04326b..4643497 100644 --- a/packages/kal-frontend/tailwind.config.ts +++ b/packages/kal-frontend/tailwind.config.ts @@ -36,6 +36,7 @@ const config: Config = { "fade-in": "fadeIn 0.5s ease-out", "slide-up": "slideUp 0.5s ease-out", "panel-slide-in": "panelSlideIn 0.25s ease-out", + "neon-pulse": "neonPulse 2s ease-in-out infinite", }, keyframes: { fadeIn: { @@ -50,6 +51,16 @@ const config: Config = { "0%": { opacity: "0", transform: "translateX(100%)" }, "100%": { opacity: "1", transform: "translateX(0)" }, }, + neonPulse: { + "0%, 100%": { + boxShadow: + "0 0 4px rgba(16,185,129,0.4), 0 0 8px rgba(16,185,129,0.2)", + }, + "50%": { + boxShadow: + "0 0 8px rgba(16,185,129,0.6), 0 0 16px rgba(16,185,129,0.3)", + }, + }, }, }, }, From 6b3a66993da4778bbdc8a4b6c8e3d88d260645a9 Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 00:53:41 +0800 Subject: [PATCH 02/11] feat: replace active API keys card with minute usage stats - Add minuteUsed to getUsageStats backend response (stale-aware) - Replace Active API Keys card with Minute Usage progress bar - Dashboard now shows: Tier, Daily, Monthly, and Minute usage --- packages/kal-backend/src/routers/api-keys.ts | 12 +++++++++ .../kal-frontend/src/app/dashboard/client.tsx | 27 ++++++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/kal-backend/src/routers/api-keys.ts b/packages/kal-backend/src/routers/api-keys.ts index fca0a58..ce9f62b 100644 --- a/packages/kal-backend/src/routers/api-keys.ts +++ b/packages/kal-backend/src/routers/api-keys.ts @@ -200,11 +200,23 @@ export const apiKeysRouter = router({ isRevoked: false, }); + // Minute usage — only valid if minuteWindow is within the current minute + const now = new Date(); + const currentMinuteStart = new Date(now); + currentMinuteStart.setSeconds(0, 0); + + const minuteWindowIsStale = + !usage?.minuteWindow || + new Date(usage.minuteWindow).getTime() < currentMinuteStart.getTime(); + + const minuteUsed = minuteWindowIsStale ? 0 : usage?.minuteCount || 0; + return { tier: user?.tier || "free", dailyUsed: usage?.dailyCount || 0, monthlyUsed, activeKeyCount, + minuteUsed, }; }), diff --git a/packages/kal-frontend/src/app/dashboard/client.tsx b/packages/kal-frontend/src/app/dashboard/client.tsx index a86ab90..8663cf2 100644 --- a/packages/kal-frontend/src/app/dashboard/client.tsx +++ b/packages/kal-frontend/src/app/dashboard/client.tsx @@ -70,6 +70,12 @@ function DashboardContent({ nameProp }: { nameProp?: string | null }) { 100, (monthlyUsed / limits.monthlyLimit) * 100 ); + const minuteUsed = stats?.minuteUsed || 0; + const minuteRemaining = Math.max(0, limits.minuteLimit - minuteUsed); + const minutePercentage = Math.min( + 100, + (minuteUsed / limits.minuteLimit) * 100 + ); const displayName = userInfo?.name || nameProp || "Developer"; @@ -157,18 +163,21 @@ function DashboardContent({ nameProp }: { nameProp?: string | null }) {
- Active API Keys + Minute Usage - - {stats?.activeKeyCount || 0} + + {minuteUsed} / {limits.minuteLimit}
- - Manage keys → - +
+
+
+

+ {minuteRemaining} remaining · resets every minute +

From 11834e440674417bc96ad2d0a34432052349c277 Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 00:59:30 +0800 Subject: [PATCH 03/11] refactor: replace rate limit stats with key-focused stats on API keys page - Add getKeyStats endpoint with active, revoked, expired, and total counts - Replace tier/daily/monthly usage cards with key stat cards - API keys page now focused on key management, usage stats live on dashboard --- packages/kal-backend/src/routers/api-keys.ts | 40 +++ .../src/app/dashboard/api-keys/client.tsx | 248 ++++++++++++------ 2 files changed, 207 insertions(+), 81 deletions(-) diff --git a/packages/kal-backend/src/routers/api-keys.ts b/packages/kal-backend/src/routers/api-keys.ts index ce9f62b..56c33c0 100644 --- a/packages/kal-backend/src/routers/api-keys.ts +++ b/packages/kal-backend/src/routers/api-keys.ts @@ -220,6 +220,46 @@ export const apiKeysRouter = router({ }; }), + /** + * Get key-specific stats: active, revoked, and expired counts + */ + getKeyStats: protectedProcedure.query(async ({ ctx }) => { + const now = new Date(); + const keysCollection = ctx.db.collection("api_keys"); + + const [activeCount, revokedCount, expiredCount, totalCount] = + await Promise.all([ + // Active: not revoked and not expired + keysCollection.countDocuments({ + userId: ctx.userId, + isRevoked: false, + $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }], + }), + // Revoked + keysCollection.countDocuments({ + userId: ctx.userId, + isRevoked: true, + }), + // Expired: not revoked but past expiration + keysCollection.countDocuments({ + userId: ctx.userId, + isRevoked: false, + expiresAt: { $ne: null, $lte: now }, + }), + // Total ever created + keysCollection.countDocuments({ + userId: ctx.userId, + }), + ]); + + return { + active: activeCount, + revoked: revokedCount, + expired: expiredCount, + total: totalCount, + }; + }), + /** * Admin: Get API key counts per user (all users) */ diff --git a/packages/kal-frontend/src/app/dashboard/api-keys/client.tsx b/packages/kal-frontend/src/app/dashboard/api-keys/client.tsx index 53843ee..e7b1290 100644 --- a/packages/kal-frontend/src/app/dashboard/api-keys/client.tsx +++ b/packages/kal-frontend/src/app/dashboard/api-keys/client.tsx @@ -1,9 +1,16 @@ "use client"; import type { ApiKeyExpiration } from "kal-shared"; -import { RATE_LIMITS } from "kal-shared"; import { useState } from "react"; -import { AlertTriangle, Check, CheckCircle, Plus } from "react-feather"; +import { + AlertTriangle, + Check, + CheckCircle, + Key, + Plus, + Slash, + Clock, +} from "react-feather"; import { useBreakpoint } from "@/hooks/useBreakpoint"; import { AuthUpdater, useAuth } from "@/lib/auth-context"; @@ -26,7 +33,11 @@ interface ApiKeysClientProps { name?: string | null; } -export default function ApiKeysClient({ logtoId, email, name }: ApiKeysClientProps) { +export default function ApiKeysClient({ + logtoId, + email, + name, +}: ApiKeysClientProps) { return ( <> @@ -35,9 +46,13 @@ export default function ApiKeysClient({ logtoId, email, name }: ApiKeysClientPro ); } -function ApiKeysContentWrapper({ expectedLogtoId }: { expectedLogtoId?: string }) { +function ApiKeysContentWrapper({ + expectedLogtoId, +}: { + expectedLogtoId?: string; +}) { const { logtoId } = useAuth(); - + if (expectedLogtoId && logtoId !== expectedLogtoId) { return (
@@ -56,12 +71,14 @@ function ApiKeysContent() { const { isMobile } = useBreakpoint(); const [showGenerateModal, setShowGenerateModal] = useState(false); const [newKeyName, setNewKeyName] = useState(""); - const [newKeyExpiration, setNewKeyExpiration] = useState<"1_week" | "1_month" | "never">("1_month"); + const [newKeyExpiration, setNewKeyExpiration] = useState< + "1_week" | "1_month" | "never" + >("1_month"); const [generatedKey, setGeneratedKey] = useState(null); const [copied, setCopied] = useState(false); const { data: apiKeys, refetch: refetchKeys } = trpc.apiKeys.list.useQuery(); - const { data: stats } = trpc.apiKeys.getUsageStats.useQuery(); + const { data: keyStats } = trpc.apiKeys.getKeyStats.useQuery(); const generateMutation = trpc.apiKeys.generate.useMutation({ onSuccess: (data) => { @@ -101,76 +118,83 @@ function ApiKeysContent() { }; const handleRevoke = (keyId: string, keyName: string) => { - if (confirm(`Are you sure you want to revoke "${keyName}"? This action cannot be undone.`)) { + if ( + confirm( + `Are you sure you want to revoke "${keyName}"? This action cannot be undone.` + ) + ) { revokeMutation.mutate({ keyId }); } }; - const tier = stats?.tier || "free"; - const limits = RATE_LIMITS[tier]; - const dailyUsed = stats?.dailyUsed || 0; - const dailyRemaining = Math.max(0, limits.dailyLimit - dailyUsed); - const dailyPercentage = Math.min(100, (dailyUsed / limits.dailyLimit) * 100); - const monthlyUsed = stats?.monthlyUsed || 0; - const monthlyRemaining = Math.max(0, limits.monthlyLimit - monthlyUsed); - const monthlyPercentage = Math.min(100, (monthlyUsed / limits.monthlyLimit) * 100); - return (
-

API Keys

-

Manage your API keys and view rate limit analytics

+

+ API Keys +

+

+ Manage your API keys +

- {/* Usage Stats */} + {/* Key Stats */}
-

Rate Limit Analytics

-
+
-
- Current Tier - - {tier === "free" ? "Free" : tier === "tier_1" ? "Tier 1" : "Tier 2"} +
+
+ +
+ + Active
-

{limits.minuteLimit} requests/minute

-

{limits.dailyLimit.toLocaleString()} requests/day

-

{limits.monthlyLimit.toLocaleString()} requests/month

+ + {keyStats?.active ?? 0} +
-
- Today's Usage - {dailyUsed} / {limits.dailyLimit} -
-
-
+
+
+ +
+ + Revoked +
-

{dailyRemaining} requests remaining

+ + {keyStats?.revoked ?? 0} +
-
- Monthly Usage - {monthlyUsed.toLocaleString()} / {limits.monthlyLimit.toLocaleString()} -
-
-
+
+
+ +
+ + Expired +
-

{monthlyRemaining.toLocaleString()} requests remaining

+ + {keyStats?.expired ?? 0} +
-
- Active Keys - {stats?.activeKeyCount || 0} +
+
+ +
+ + Total Created +
+ + {keyStats?.total ?? 0} +
@@ -178,8 +202,10 @@ function ApiKeysContent() { {/* API Keys List */}
-

Your API Keys

-
- + {apiKeys.map((key: SerializedApiKey) => (
{/* Desktop Row */}
- {key.name} - {key.keyPrefix} + + {key.name} + + + {key.keyPrefix} + - {key.expiresAt ? new Date(key.expiresAt).toLocaleDateString() : "Never"} + {key.expiresAt + ? new Date(key.expiresAt).toLocaleDateString() + : "Never"} - {key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : "Never"} + {key.lastUsedAt + ? new Date(key.lastUsedAt).toLocaleDateString() + : "Never"}
- + {/* Mobile Card */}
- {key.name} + + {key.name} +
- {key.keyPrefix} + + {key.keyPrefix} +
- Expires: {key.expiresAt ? new Date(key.expiresAt).toLocaleDateString() : "Never"} - Used: {key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : "Never"} + + Expires:{" "} + {key.expiresAt + ? new Date(key.expiresAt).toLocaleDateString() + : "Never"} + + + Used:{" "} + {key.lastUsedAt + ? new Date(key.lastUsedAt).toLocaleDateString() + : "Never"} +
@@ -242,23 +290,35 @@ function ApiKeysContent() {
) : (
-

No API keys yet. Generate one to get started!

+

+ No API keys yet. Generate one to get started! +

)} {/* Generate Key Modal */} {showGenerateModal && ( -
-
+
e.stopPropagation()} > {!generatedKey ? ( <> -

Generate New API Key

+

+ Generate New API Key +

- +
- +
{[ { value: "1_week", label: "1 Week" }, @@ -284,7 +346,11 @@ function ApiKeysContent() { ? "bg-accent text-dark" : "bg-dark-elevated text-content-secondary border border-dark-border hover:border-accent/30" }`} - onClick={() => setNewKeyExpiration(opt.value as "1_week" | "1_month" | "never")} + onClick={() => + setNewKeyExpiration( + opt.value as "1_week" | "1_month" | "never" + ) + } > {opt.label} @@ -292,7 +358,10 @@ function ApiKeysContent() {
-
) : ( <>

- API Key Generated! + {" "} + API Key Generated!

- Save this key now! + {" "} + Save this key now! +

+

+ This is the only time you'll see this key.

-

This is the only time you'll see this key.

- {generatedKey} -
- - - Page {currentPage} of {totalPages} - + {/* Page numbers */} + {getPageNumbers(currentPage, totalPages).map((page, i) => + page === "..." ? ( + + … + + ) : ( + + ) + )} + {/* Next */}
)} From 74689810e1b878bd24ab8cc92cfb3f5b6f39c60f Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 01:10:06 +0800 Subject: [PATCH 07/11] Add v1.0.2 changelog for April 7, 2026 --- docs/changelog.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index c404cb4..cd88828 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,19 @@ All notable changes to Kal will be documented in this file. --- +## [1.0.2] - 2026-04-07 + +### Added + +- Per-minute usage progress bar in the Rate Limits documentation tab — see your real-time minute-level usage alongside daily limits +- Numbered page navigation on the Food Database — quickly jump to any page instead of clicking through one at a time + +### Fixed + +- Corrected example MongoDB IDs in the Setup page API quick reference to match actual database records + +--- + ## [1.0.1] - 2026-04-06 ### Fixed From 9bd6d972c90177b54c4f1a97f9a72ec927303e83 Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 01:15:54 +0800 Subject: [PATCH 08/11] Add live reset countdown timer to admin dashboard, fix Requests Today showing wrong data, add Food Database stat card --- packages/kal-admin/src/app/dashboard/page.tsx | 108 +++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/packages/kal-admin/src/app/dashboard/page.tsx b/packages/kal-admin/src/app/dashboard/page.tsx index ae075e5..7194735 100644 --- a/packages/kal-admin/src/app/dashboard/page.tsx +++ b/packages/kal-admin/src/app/dashboard/page.tsx @@ -1,17 +1,74 @@ "use client"; +import { useState, useEffect } from "react"; + import { trpc } from "@/lib/trpc"; +// ─── Countdown hook ─────────────────────────────────────────────────────────── +/** Returns a live "HHh MMm SSs" string counting down to midnight UTC. */ +function useCountdownToMidnightUTC(): string { + const [timeLeft, setTimeLeft] = useState(() => getMsUntilMidnightUTC()); + + useEffect(() => { + const id = setInterval(() => setTimeLeft(getMsUntilMidnightUTC()), 1_000); + return () => clearInterval(id); + }, []); + + const totalSec = Math.max(0, Math.floor(timeLeft / 1_000)); + const h = Math.floor(totalSec / 3_600); + const m = Math.floor((totalSec % 3_600) / 60); + const s = totalSec % 60; + return `${h}h ${String(m).padStart(2, "0")}m ${String(s).padStart(2, "0")}s`; +} + +function getMsUntilMidnightUTC(): number { + const now = new Date(); + const midnight = new Date(now); + midnight.setUTCDate(midnight.getUTCDate() + 1); + midnight.setUTCHours(0, 0, 0, 0); + return midnight.getTime() - now.getTime(); +} + +/** Inline countdown badge for stat cards. */ +function ResetTimer() { + const countdown = useCountdownToMidnightUTC(); + return ( + + + + + + Resets in{" "} + {countdown} + + ); +} + +// ─── Stat card ──────────────────────────────────────────────────────────────── function StatCard({ label, value, sub, + subNode, color = "primary", icon, }: { label: string; value: string | number; sub?: string; + subNode?: React.ReactNode; color?: "primary" | "success" | "warning" | "info"; icon: React.ReactNode; }) { @@ -39,6 +96,7 @@ function StatCard({ {value}

{sub &&

{sub}

} + {subNode &&
{subNode}
}
); @@ -107,9 +165,11 @@ export default function DashboardPage() { } color="info" icon={ @@ -176,6 +236,50 @@ export default function DashboardPage() { } /> + + + + + + + } + />
{/* Two column section */} From f9e7c42a08462bb41d4a319953f49968244962c1 Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 01:17:56 +0800 Subject: [PATCH 09/11] Revert "Add live reset countdown timer to admin dashboard, fix Requests Today showing wrong data, add Food Database stat card" This reverts commit 9bd6d972c90177b54c4f1a97f9a72ec927303e83. --- packages/kal-admin/src/app/dashboard/page.tsx | 108 +----------------- 1 file changed, 2 insertions(+), 106 deletions(-) diff --git a/packages/kal-admin/src/app/dashboard/page.tsx b/packages/kal-admin/src/app/dashboard/page.tsx index 7194735..ae075e5 100644 --- a/packages/kal-admin/src/app/dashboard/page.tsx +++ b/packages/kal-admin/src/app/dashboard/page.tsx @@ -1,74 +1,17 @@ "use client"; -import { useState, useEffect } from "react"; - import { trpc } from "@/lib/trpc"; -// ─── Countdown hook ─────────────────────────────────────────────────────────── -/** Returns a live "HHh MMm SSs" string counting down to midnight UTC. */ -function useCountdownToMidnightUTC(): string { - const [timeLeft, setTimeLeft] = useState(() => getMsUntilMidnightUTC()); - - useEffect(() => { - const id = setInterval(() => setTimeLeft(getMsUntilMidnightUTC()), 1_000); - return () => clearInterval(id); - }, []); - - const totalSec = Math.max(0, Math.floor(timeLeft / 1_000)); - const h = Math.floor(totalSec / 3_600); - const m = Math.floor((totalSec % 3_600) / 60); - const s = totalSec % 60; - return `${h}h ${String(m).padStart(2, "0")}m ${String(s).padStart(2, "0")}s`; -} - -function getMsUntilMidnightUTC(): number { - const now = new Date(); - const midnight = new Date(now); - midnight.setUTCDate(midnight.getUTCDate() + 1); - midnight.setUTCHours(0, 0, 0, 0); - return midnight.getTime() - now.getTime(); -} - -/** Inline countdown badge for stat cards. */ -function ResetTimer() { - const countdown = useCountdownToMidnightUTC(); - return ( - - - - - - Resets in{" "} - {countdown} - - ); -} - -// ─── Stat card ──────────────────────────────────────────────────────────────── function StatCard({ label, value, sub, - subNode, color = "primary", icon, }: { label: string; value: string | number; sub?: string; - subNode?: React.ReactNode; color?: "primary" | "success" | "warning" | "info"; icon: React.ReactNode; }) { @@ -96,7 +39,6 @@ function StatCard({ {value}

{sub &&

{sub}

} - {subNode &&
{subNode}
}
); @@ -165,11 +107,9 @@ export default function DashboardPage() { } + sub={`${foodStats?.foods ?? 0} natural · ${foodStats?.halal ?? 0} halal`} color="info" icon={ @@ -236,50 +176,6 @@ export default function DashboardPage() { } /> - - - - - - - } - /> {/* Two column section */} From c539e1f24870bc5636c0f2f9531901b68c0cc3e2 Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 01:20:22 +0800 Subject: [PATCH 10/11] Add live reset countdown timers to dashboard usage stat cards --- .../kal-frontend/src/app/dashboard/client.tsx | 103 ++++++++++++++++-- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/kal-frontend/src/app/dashboard/client.tsx b/packages/kal-frontend/src/app/dashboard/client.tsx index 8663cf2..3f2efc3 100644 --- a/packages/kal-frontend/src/app/dashboard/client.tsx +++ b/packages/kal-frontend/src/app/dashboard/client.tsx @@ -2,13 +2,83 @@ import { RATE_LIMITS } from "kal-shared"; import Link from "next/link"; -import { Book, Code, Key, List, Search, Settings } from "react-feather"; +import { useState, useEffect } from "react"; +import { Book, Clock, Code, Key, List, Search, Settings } from "react-feather"; import { UsageChart } from "@/components/dashboard/UsageChart"; import { useBreakpoint } from "@/hooks/useBreakpoint"; import { AuthUpdater, useAuth } from "@/lib/auth-context"; import { trpc } from "@/lib/trpc"; +// ─── Countdown helpers ──────────────────────────────────────────────────────── +function getMsUntilMidnightUTC(): number { + const now = new Date(); + const midnight = new Date(now); + midnight.setUTCDate(midnight.getUTCDate() + 1); + midnight.setUTCHours(0, 0, 0, 0); + return midnight.getTime() - now.getTime(); +} + +function getMsUntilNextMonthUTC(): number { + const now = new Date(); + const nextMonth = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1) + ); + return nextMonth.getTime() - now.getTime(); +} + +function getMsUntilNextMinute(): number { + const now = new Date(); + return (60 - now.getSeconds()) * 1_000 - now.getMilliseconds(); +} + +function formatCountdown(ms: number, compact = false): string { + const totalSec = Math.max(0, Math.floor(ms / 1_000)); + + if (compact) { + const s = totalSec % 60; + return `${s}s`; + } + + const d = Math.floor(totalSec / 86_400); + const h = Math.floor((totalSec % 86_400) / 3_600); + const m = Math.floor((totalSec % 3_600) / 60); + const s = totalSec % 60; + + if (d > 0) { + return `${d}d ${h}h ${String(m).padStart(2, "0")}m`; + } + return `${h}h ${String(m).padStart(2, "0")}m ${String(s).padStart(2, "0")}s`; +} + +function useCountdown(getMsFn: () => number, interval = 1_000): number { + const [ms, setMs] = useState(getMsFn); + + useEffect(() => { + const id = setInterval(() => setMs(getMsFn()), interval); + return () => clearInterval(id); + }, [getMsFn, interval]); + + return ms; +} + +function ResetBadge({ + ms, + compact = false, +}: { + ms: number; + compact?: boolean; +}) { + return ( + + + + {formatCountdown(ms, compact)} + + + ); +} + interface DashboardClientProps { logtoId?: string; email?: string | null; @@ -59,6 +129,10 @@ function DashboardContent({ nameProp }: { nameProp?: string | null }) { const { data: chartData, isLoading: isLoadingChart } = trpc.requestLogs.requestsByDay.useQuery({ days: 30 }); + const dailyResetMs = useCountdown(getMsUntilMidnightUTC); + const monthlyResetMs = useCountdown(getMsUntilNextMonthUTC); + const minuteResetMs = useCountdown(getMsUntilNextMinute, 1_000); + const tier = stats?.tier || "free"; const limits = RATE_LIMITS[tier]; const dailyUsed = stats?.dailyUsed || 0; @@ -134,9 +208,12 @@ function DashboardContent({ nameProp }: { nameProp?: string | null }) { style={{ width: `${dailyPercentage}%` }} /> -

- {dailyRemaining} remaining -

+
+

+ {dailyRemaining} remaining +

+ +
@@ -155,9 +232,12 @@ function DashboardContent({ nameProp }: { nameProp?: string | null }) { style={{ width: `${monthlyPercentage}%` }} />
-

- {monthlyRemaining.toLocaleString()} remaining -

+
+

+ {monthlyRemaining.toLocaleString()} remaining +

+ +
@@ -175,9 +255,12 @@ function DashboardContent({ nameProp }: { nameProp?: string | null }) { style={{ width: `${minutePercentage}%` }} />
-

- {minuteRemaining} remaining · resets every minute -

+
+

+ {minuteRemaining} remaining +

+ +
From 796cbc3db91b9f86ea44420ca108f26b8c4c28af Mon Sep 17 00:00:00 2001 From: Zen0space Date: Tue, 7 Apr 2026 01:23:34 +0800 Subject: [PATCH 11/11] Update v1.0.2 changelog with reset countdown timers --- docs/changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index cd88828..4c728c2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,10 +10,11 @@ All notable changes to Kal will be documented in this file. - Per-minute usage progress bar in the Rate Limits documentation tab — see your real-time minute-level usage alongside daily limits - Numbered page navigation on the Food Database — quickly jump to any page instead of clicking through one at a time +- Live reset countdown timers on dashboard usage cards — see exactly when your daily, monthly, and per-minute limits reset ### Fixed -- Corrected example MongoDB IDs in the Setup page API quick reference to match actual database records +- Corrected example Food IDs in the Setup page API quick reference to match actual records ---