Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions src/apps/admin/components/DashboardPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ function formatNumber(n: number): string {
}

function formatDateLabel(dateStr: string): string {
const d = new Date(dateStr + "T00:00:00Z");
return d.toLocaleDateString(undefined, {
const [y, m, d] = dateStr.split("-").map(Number);
const local = new Date(y, m - 1, d);
return local.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
timeZone: "UTC",
});
}

Expand Down Expand Up @@ -155,7 +155,7 @@ export function DashboardPanel({ onRefresh }: DashboardPanelProps) {
const [data, setData] = useState<AnalyticsDetail | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [rangeDays, setRangeDays] = useState(7);
const [rangeDays, setRangeDays] = useState(1);

const fetchData = useCallback(async () => {
if (!username || !isAuthenticated) return;
Expand Down Expand Up @@ -233,6 +233,11 @@ export function DashboardPanel({ onRefresh }: DashboardPanelProps) {
? `${((totals.errors / totals.calls) * 100).toFixed(1)}%`
: "0%";

const todayErrorRate =
todayData && todayData.calls > 0
? `${((todayData.errors / todayData.calls) * 100).toFixed(1)}%`
: "0%";

const topEndpointMax = topEndpoints.length > 0 ? topEndpoints[0].count : 1;

return (
Expand All @@ -241,7 +246,7 @@ export function DashboardPanel({ onRefresh }: DashboardPanelProps) {
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-200 bg-gray-50 flex-shrink-0">
<span className="text-[12px] font-medium">Dashboard</span>
<div className="flex items-center gap-1">
{[7, 14, 30].map((d) => (
{[1, 7, 14, 30].map((d) => (
<Button
key={d}
variant="ghost"
Expand All @@ -252,7 +257,7 @@ export function DashboardPanel({ onRefresh }: DashboardPanelProps) {
rangeDays === d && "bg-neutral-200"
)}
>
{d}d
{d === 1 ? "Today" : `${d}d`}
</Button>
))}
<Button
Expand All @@ -277,37 +282,46 @@ export function DashboardPanel({ onRefresh }: DashboardPanelProps) {
{/* KPI Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2 p-3">
<StatCard
label="Visitors"
value={formatNumber(todayData?.uniqueVisitors ?? 0)}
trend={calcTrend(
label={rangeDays === 1 ? "Visitors (today)" : `Visitors (${rangeDays}d)`}
value={formatNumber(totals.uniqueVisitors)}
trend={yesterdayData ? calcTrend(
todayData?.uniqueVisitors,
yesterdayData?.uniqueVisitors
)}
) : undefined}
/>
<StatCard
label="API Calls"
value={formatNumber(todayData?.calls ?? 0)}
trend={calcTrend(todayData?.calls, yesterdayData?.calls)}
label={rangeDays === 1 ? "API Calls (today)" : `API Calls (${rangeDays}d)`}
value={formatNumber(totals.calls)}
trend={yesterdayData ? calcTrend(todayData?.calls, yesterdayData?.calls) : undefined}
/>
<StatCard
label="AI Requests"
value={formatNumber(todayData?.ai ?? 0)}
trend={calcTrend(todayData?.ai, yesterdayData?.ai)}
label={rangeDays === 1 ? "AI Requests (today)" : `AI Requests (${rangeDays}d)`}
value={formatNumber(totals.ai)}
trend={yesterdayData ? calcTrend(todayData?.ai, yesterdayData?.ai) : undefined}
/>
<StatCard
label="Error Rate"
value={errorRate}
trend={calcTrend(todayData?.errors, yesterdayData?.errors)}
trend={yesterdayData ? calcTrend(todayData?.errors, yesterdayData?.errors) : undefined}
/>
</div>

{/* Totals strip */}
{/* Context strip */}
<div className="flex items-center gap-4 px-4 pb-2 text-[10px] text-neutral-400">
<span>
{rangeDays}d totals: {formatNumber(totals.calls)} calls
</span>
<span>{formatNumber(totals.uniqueVisitors)} visitors</span>
<span>{formatNumber(totals.ai)} AI</span>
{rangeDays > 1 ? (
<>
<span>
today: {formatNumber(todayData?.calls ?? 0)} calls
</span>
<span>{formatNumber(todayData?.uniqueVisitors ?? 0)} visitors</span>
<span>{formatNumber(todayData?.ai ?? 0)} AI</span>
<span>{todayErrorRate} err</span>
</>
) : (
<span>
{formatNumber(totals.errors)} errors
</span>
)}
<span>{totals.avgLatencyMs}ms avg</span>
</div>

Expand Down Expand Up @@ -387,7 +401,7 @@ export function DashboardPanel({ onRefresh }: DashboardPanelProps) {
<div className="border border-gray-200 rounded bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-gray-100 bg-gray-50">
<span className="text-[10px] uppercase tracking-wide text-neutral-400">
Top Endpoints ({rangeDays}d)
Top Endpoints ({rangeDays === 1 ? "today" : `${rangeDays}d`})
</span>
</div>
{topEndpoints.length === 0 ? (
Expand Down
5 changes: 5 additions & 0 deletions src/apps/calendar/hooks/useCalendarLogic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState, useMemo, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { track } from "@vercel/analytics";
import { CALENDAR_ANALYTICS } from "@/utils/analytics";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useThemeStore } from "@/stores/useThemeStore";
import {
Expand Down Expand Up @@ -453,6 +455,7 @@ export function useCalendarLogic() {
const created = events.find((e) => e.id === id) ??
({ ...eventData, id, createdAt: Date.now(), updatedAt: Date.now() } as CalendarEvent);
pushUndo({ type: "addEvent", event: created });
track(CALENDAR_ANALYTICS.EVENT_CREATE);
}
setIsEventDialogOpen(false);
setEditingEvent(null);
Expand All @@ -478,6 +481,7 @@ export function useCalendarLogic() {
if (ev) pushUndo({ type: "deleteEvent", event: { ...ev } });
deleteEvent(selectedEventId);
setSelectedEventId(null);
track(CALENDAR_ANALYTICS.EVENT_DELETE);
}
}, [selectedEventId, events, deleteEvent, pushUndo]);

Expand All @@ -488,6 +492,7 @@ export function useCalendarLogic() {
setIsEventDialogOpen(false);
setEditingEvent(null);
setPrefillTime(null);
track(CALENDAR_ANALYTICS.EVENT_DELETE);
}
}, [editingEvent, deleteEvent, pushUndo]);

Expand Down
4 changes: 4 additions & 0 deletions src/apps/contacts/hooks/useContactsLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { track } from "@vercel/analytics";
import { CONTACTS_ANALYTICS } from "@/utils/analytics";
import { useShallow } from "zustand/react/shallow";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useThemeStore } from "@/stores/useThemeStore";
Expand Down Expand Up @@ -137,13 +139,15 @@ export function useContactsLogic() {
setSelectedGroupId("all");
const id = addContact({ source: "manual" });
setSelectedContactId(id);
track(CONTACTS_ANALYTICS.CONTACT_CREATE);
};

const handleDeleteSelectedContact = () => {
if (!selectedContact) {
return;
}
deleteContact(selectedContact.id);
track(CONTACTS_ANALYTICS.CONTACT_DELETE);
toast.success(
t("apps.contacts.messages.deleted", {
name: selectedContact.displayName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { requestCloudSyncCheck } from "@/utils/cloudSyncEvents";
import { PaperPlaneRight } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { useRealtimeConnectionStatus } from "@/hooks/useRealtimeConnectionStatus";
import { track } from "@vercel/analytics";
import { SETTINGS_ANALYTICS } from "@/utils/analytics";

// Version display component that reads from app store
function VersionDisplay() {
Expand Down Expand Up @@ -516,7 +518,10 @@ export function ControlPanelsAppComponent({
</div>
<Select
value={currentTheme}
onValueChange={(value) => setTheme(value as OsThemeId)}
onValueChange={(value) => {
setTheme(value as OsThemeId);
track(SETTINGS_ANALYTICS.THEME_CHANGE, { theme: value });
}}
>
<SelectTrigger className="w-[120px] flex-shrink-0">
<SelectValue placeholder={t("apps.control-panels.select")}>
Expand Down
3 changes: 3 additions & 0 deletions src/apps/control-panels/components/WallpaperPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useSound, Sounds } from "@/hooks/useSound";
import type { DisplayMode } from "@/utils/displayMode";
import { Plus, Trash } from "@phosphor-icons/react";
import { useDisplaySettingsStore } from "@/stores/useDisplaySettingsStore";
import { track } from "@vercel/analytics";
import { SETTINGS_ANALYTICS } from "@/utils/analytics";
import { loadWallpaperManifest } from "@/utils/wallpapers";
import type { WallpaperManifest as WallpaperManifestType } from "@/utils/wallpapers";
import { useTranslation } from "react-i18next";
Expand Down Expand Up @@ -261,6 +263,7 @@ export function WallpaperPicker({ onSelect }: WallpaperPickerProps) {
const handleWallpaperSelect = (path: string) => {
setWallpaper(path);
playClick();
track(SETTINGS_ANALYTICS.WALLPAPER_CHANGE, { wallpaper: path });
if (onSelect) {
onSelect(path);
}
Expand Down
11 changes: 7 additions & 4 deletions src/apps/finder/hooks/useFinderLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
compareFinderItemsByDisplayName,
compareFinderSortText,
} from "@/utils/finderDisplay";
import { track } from "@vercel/analytics";
import { FINDER_ANALYTICS } from "@/utils/analytics";
import { helpItems } from "../index";
import { useFilesStoreShallow } from "@/stores/helpers";
import { useDockStore } from "@/stores/useDockStore";
Expand Down Expand Up @@ -415,15 +417,14 @@ export function useFinderLogic({
originalPath: file.path,
});
moveToTrash(file);
track(FINDER_ANALYTICS.FILE_DELETE, { name: file.name });
},
[moveToTrash, pushUndoAction]
);

// Wrap the original handleFileOpen - now only calls the original without TextEditStore updates
const handleFileOpen = async (file: FileItem, launchOrigin?: LaunchOriginRect) => {
// Call original file open handler from useFileSystem
originalHandleFileOpen(file, launchOrigin);
// TextEditStore updates removed - TextEdit instances now manage their own state
track(FINDER_ANALYTICS.FILE_OPEN, { name: file.name, isDirectory: file.isDirectory });
};

// Use the original saveFile directly without TextEditStore updates
Expand Down Expand Up @@ -498,6 +499,7 @@ export function useFinderLogic({
const confirmEmptyTrash = () => {
emptyTrash();
setIsEmptyTrashDialogOpen(false);
track(FINDER_ANALYTICS.TRASH_EMPTY);
};

const handleNewWindow = () => {
Expand Down Expand Up @@ -827,6 +829,7 @@ export function useFinderLogic({
oldName: selectedFile.name,
newName: trimmedNewName,
});
track(FINDER_ANALYTICS.FILE_RENAME);

emitFileRenamed({
oldPath: oldPathForRename,
Expand Down Expand Up @@ -935,8 +938,8 @@ export function useFinderLogic({
const basePath = currentPath === "/" ? "" : currentPath;
const newPath = `${basePath}/${trimmedName}`;

// Use the createFolder function from the hook
createFolder({ path: newPath, name: trimmedName });
track(FINDER_ANALYTICS.FOLDER_CREATE);

setIsNewFolderDialogOpen(false);
};
Expand Down
6 changes: 6 additions & 0 deletions src/apps/minesweeper/hooks/useMinesweeperLogic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { track } from "@vercel/analytics";
import { MINESWEEPER_ANALYTICS } from "@/utils/analytics";
import { helpItems } from "..";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useSound, Sounds } from "@/hooks/useSound";
Expand Down Expand Up @@ -170,6 +172,7 @@ export function useMinesweeperLogic() {
if (allNonMinesRevealed) {
playGameWin();
setGameWon(true);
track(MINESWEEPER_ANALYTICS.GAME_WIN);
}
},
[playGameWin]
Expand Down Expand Up @@ -230,6 +233,7 @@ export function useMinesweeperLogic() {
playMineHit();
revealAllMines(newBoard);
setGameOver(true);
track(MINESWEEPER_ANALYTICS.GAME_LOSE);
return;
}
}
Expand All @@ -242,6 +246,7 @@ export function useMinesweeperLogic() {
playMineHit();
revealAllMines(newBoard);
setGameOver(true);
track(MINESWEEPER_ANALYTICS.GAME_LOSE);
return;
}

Expand Down Expand Up @@ -286,6 +291,7 @@ export function useMinesweeperLogic() {
setGameWon(false);
setIsNewGameDialogOpen(false);
setRemainingMines(MINES_COUNT);
track(MINESWEEPER_ANALYTICS.GAME_START);
}, [initializeBoard]);

const currentTheme = useThemeStore((state) => state.current);
Expand Down
4 changes: 4 additions & 0 deletions src/apps/paint/hooks/usePaintLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { usePaintStore } from "@/stores/usePaintStore";
import type { Filter } from "../components/PaintFiltersMenu";
import { useAppStore } from "@/stores/useAppStore";
import { toast } from "sonner";
import { track } from "@vercel/analytics";
import { PAINT_ANALYTICS } from "@/utils/analytics";
import { useThemeStore } from "@/stores/useThemeStore";
import { useTranslation } from "react-i18next";
import { helpItems } from "..";
Expand Down Expand Up @@ -270,6 +272,7 @@ export function usePaintLogic({ initialData, instanceId }: UsePaintLogicProps) {
setLastFilePath(filePath);
setHasUnsavedChanges(false);
setIsSaveDialogOpen(false);
track(PAINT_ANALYTICS.FILE_SAVE);
toast.success("Image saved successfully");
} catch (err) {
console.error("Error saving file:", err);
Expand Down Expand Up @@ -298,6 +301,7 @@ export function usePaintLogic({ initialData, instanceId }: UsePaintLogicProps) {
document.body.removeChild(link);

setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
track(PAINT_ANALYTICS.FILE_EXPORT);
} catch (err) {
console.error("Error exporting file:", err);
}
Expand Down
4 changes: 4 additions & 0 deletions src/apps/photo-booth/hooks/usePhotoBoothLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { useThemeStore } from "@/stores/useThemeStore";
import { getTranslatedAppName } from "@/utils/i18n";
import { useLatestRef } from "@/hooks/useLatestRef";
import { useTimeout } from "@/hooks/useTimeout";
import { track } from "@vercel/analytics";
import { PHOTO_BOOTH_ANALYTICS } from "@/utils/analytics";
import { helpItems } from "..";
import { useShallow } from "zustand/react/shallow";

Expand Down Expand Up @@ -294,6 +296,7 @@ export function usePhotoBoothLogic({
console.log("No photos to export");
return;
}
track(PHOTO_BOOTH_ANALYTICS.EXPORT, { count: photos.length });

// If there's only one photo, download it directly
if (photos.length === 1) {
Expand Down Expand Up @@ -1004,6 +1007,7 @@ export function usePhotoBoothLogic({
const triggerCapture = useCallback(() => {
const event = new CustomEvent("webcam-capture");
window.dispatchEvent(event);
track(PHOTO_BOOTH_ANALYTICS.CAPTURE);
}, []);

return {
Expand Down
4 changes: 4 additions & 0 deletions src/apps/stickies/hooks/useStickiesLogic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { track } from "@vercel/analytics";
import { STICKIES_ANALYTICS } from "@/utils/analytics";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useThemeStore } from "@/stores/useThemeStore";
import { useStickiesStore, StickyColor } from "@/stores/useStickiesStore";
Expand Down Expand Up @@ -43,10 +45,12 @@ export function useStickiesLogic() {
const handleCreateNote = useCallback((color?: StickyColor) => {
const newId = addNote(color);
setSelectedNoteId(newId);
track(STICKIES_ANALYTICS.NOTE_CREATE);
}, [addNote]);

const handleDeleteNote = useCallback((id: string) => {
deleteNote(id);
track(STICKIES_ANALYTICS.NOTE_DELETE);
if (selectedNoteId === id) {
setSelectedNoteId(null);
}
Expand Down
Loading
Loading