From 03dfa6a5ae6d128497c317a6fdf100a006c7f901 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 5 Mar 2026 17:42:03 +0000 Subject: [PATCH] feat(ui): add activity log page backed by actions_log --- app/dashboard/activity/page.tsx | 297 ++++++++++++++++++++++++++++++++ app/dashboard/page.tsx | 11 +- 2 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 app/dashboard/activity/page.tsx diff --git a/app/dashboard/activity/page.tsx b/app/dashboard/activity/page.tsx new file mode 100644 index 0000000..aac9acd --- /dev/null +++ b/app/dashboard/activity/page.tsx @@ -0,0 +1,297 @@ +import Link from "next/link"; + +import { getServerAgencyContextOrRedirect } from "@/lib/agency"; +import { createSupabaseServerClient } from "@/lib/supabase/server"; +import { formatDateTime } from "@/lib/dates"; + +const PAGE_SIZE = 50; + +type SearchParams = { + page?: string; + instance?: string; + action?: string; + status?: string; +}; + +function getSingleParam(value: string | string[] | undefined) { + if (!value) return ""; + return Array.isArray(value) ? (value[0] ?? "") : value; +} + +function buildQueryString(next: Record) { + const params = new URLSearchParams(); + + for (const [key, value] of Object.entries(next)) { + if (value == null) continue; + const normalized = String(value); + if (!normalized) continue; + params.set(key, normalized); + } + + return params.toString(); +} + +export default async function ActivityPage({ + searchParams, +}: { + searchParams?: SearchParams; +}) { + const context = await getServerAgencyContextOrRedirect(); + const supabase = createSupabaseServerClient() as any; + + const selectedInstance = getSingleParam(searchParams?.instance); + const selectedAction = getSingleParam(searchParams?.action); + const selectedStatus = getSingleParam(searchParams?.status); + const page = Math.max( + Number(getSingleParam(searchParams?.page) || "1") || 1, + 1, + ); + + const { data: instances } = await supabase + .from("plesk_instances") + .select("id, name, base_url") + .eq("agency_id", context.agencyId) + .order("name", { ascending: true }); + + let baseQuery = supabase + .from("actions_log") + .select( + "id, created_at, target_type, target_id, action, status, error_message, metadata", + { count: "exact" }, + ) + .eq("agency_id", context.agencyId) + .order("created_at", { ascending: false }); + + if (selectedAction) { + baseQuery = baseQuery.eq("action", selectedAction); + } + + if (selectedStatus) { + baseQuery = baseQuery.eq("status", selectedStatus); + } + + if (selectedInstance) { + baseQuery = baseQuery.or( + `target_id.eq.${selectedInstance},metadata->>plesk_instance_id.eq.${selectedInstance}`, + ); + } + + const from = (page - 1) * PAGE_SIZE; + const to = from + PAGE_SIZE - 1; + + const { data: logs, count } = await baseQuery.range(from, to); + + const { data: recentForActions } = await supabase + .from("actions_log") + .select("action") + .eq("agency_id", context.agencyId) + .order("created_at", { ascending: false }) + .limit(200); + + const actionOptions = Array.from( + new Set((recentForActions ?? []).map((entry: any) => String(entry.action))), + ).sort(); + + const instanceNameById = new Map( + (instances ?? []).map((instance: any) => [ + String(instance.id), + String(instance.name ?? instance.base_url ?? instance.id), + ]), + ); + + const total = count ?? 0; + const hasPrev = page > 1; + const hasNext = to + 1 < total; + + return ( +
+
+
+

Activity Log

+

+ Recent actions for your agency +

+
+ + Back to dashboard + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Reset + +
+
+ +
+ + + + + + + + + + + + + + {(logs ?? []).length === 0 ? ( + + + + ) : ( + (logs ?? []).map((row: any) => { + const metadata = + row?.metadata && typeof row.metadata === "object" + ? (row.metadata as Record) + : null; + + const instanceId = + (metadata?.plesk_instance_id as string | undefined) ?? + (row.target_type === "plesk_instance" + ? (row.target_id as string | null) + : null); + + return ( + + + + + + + + + + ); + }) + )} + +
TimeInstanceActionTargetStatusMessageDetails
+ No activity found. +
+ {formatDateTime(row.created_at)} + + {instanceId + ? (instanceNameById.get(instanceId) ?? instanceId) + : "-"} + {row.action ?? "-"} + {row.target_type ?? "-"} + {row.target_id ? ` (${row.target_id})` : ""} + {row.status ?? "-"}{row.error_message ?? "-"} + {metadata ? ( +
+ + View + +
+                            {JSON.stringify(metadata, null, 2)}
+                          
+
+ ) : ( + "-" + )} +
+
+ +
+

+ Showing {Math.min(total, from + 1)} - {Math.min(total, to + 1)} of{" "} + {total} +

+
+ {hasPrev ? ( + + Previous + + ) : ( + + Previous + + )} + {hasNext ? ( + + Next + + ) : ( + + Next + + )} +
+
+
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index f82bb2a..bafd75e 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -3,6 +3,7 @@ import { DashboardControls } from "@/components/dashboard/dashboard-controls"; import { getServerAgencyContextOrRedirect } from "@/lib/agency"; import type { Database } from "@/lib/types"; import { createSupabaseServerClient } from "@/lib/supabase/server"; +import Link from "next/link"; type AgencySummary = Pick< Database["public"]["Tables"]["agencies"]["Row"], @@ -238,7 +239,15 @@ export default async function DashboardPage() {

Role: {context.role}

- +
+ + Activity + + +
-- 2.39.5