+
+
+
+
+
+
+
+ setFilterStartDate(e.target.value)}
+ className="form-input w-36 text-sm"
+ />
+
+
+
+ setFilterEndDate(e.target.value)}
+ className="form-input w-36 text-sm"
+ />
+
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+ {logs.length === 0 && !isLoading ? (
+
+
π
+
No activity logged yet.
+
+ ) : (
+
+
+
+
+ |
+ Time
+ |
+
+ User
+ |
+
+ Action
+ |
+
+ Details
+ |
+
+
+
+ {logs.map((log) => (
+
+ |
+ {formatDate(log.createdAt)}
+ |
+
+ {log.username}
+ |
+
+
+ {ACTION_TYPE_LABELS[log.action] ?? log.action}
+
+ |
+
+ {parseDetails(log.details)}
+ |
+
+ ))}
+
+
+
+ )}
+
+ {isLoading && (
+
Loadingβ¦
+ )}
+
+ {hasMore && !isLoading && logs.length > 0 && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/server/ServerCard.tsx b/src/components/server/ServerCard.tsx
index 66ccea5..7286b9a 100755
--- a/src/components/server/ServerCard.tsx
+++ b/src/components/server/ServerCard.tsx
@@ -77,10 +77,15 @@ export function ServerCard({ server }: ServerCardProps) {
-
+
{serviceStatusToString(server.status)}
+ {server.lastActivity && (
+
+ by {server.lastActivity.username}
+
+ )}
diff --git a/src/components/server/ServerConfigurationTabs.tsx b/src/components/server/ServerConfigurationTabs.tsx
index 338a8dc..a8f1229 100755
--- a/src/components/server/ServerConfigurationTabs.tsx
+++ b/src/components/server/ServerConfigurationTabs.tsx
@@ -8,6 +8,7 @@ import { EventRulesEditor } from '@/components/configuration/EventRulesEditor';
import { ServerSettingsEditor } from '@/components/configuration/ServerSettingsEditor';
import { StatisticsDashboard } from '@/components/statistics/StatisticsDashboard';
import { LeaderboardManager } from '@/components/leaderboard/LeaderboardManager';
+import { ActivityLogTable } from '@/components/activityLog/ActivityLogTable';
import { useState } from 'react';
import { StateHistoryStats } from '@/lib/schemas';
import { Leaderboard } from '@/lib/schemas/leaderboard';
@@ -26,7 +27,8 @@ const tabs = [
{ id: ServerTab.event, name: 'Event Config', icon: 'π' },
{ id: ServerTab.eventRules, name: 'Event Rules', icon: 'π' },
{ id: ServerTab.settings, name: 'Server Settings', icon: 'π§' },
- { id: ServerTab.leaderboard, name: 'Leaderboard', icon: 'π' }
+ { id: ServerTab.leaderboard, name: 'Leaderboard', icon: 'π' },
+ { id: ServerTab.activityLog, name: 'History', icon: 'π' }
];
const configTabs = new Set([
@@ -138,6 +140,9 @@ export function ServerConfigurationTabs({
case ServerTab.leaderboard:
return
;
+ case ServerTab.activityLog:
+ return
;
+
default:
return (
diff --git a/src/components/ui/Sidebar.tsx b/src/components/ui/Sidebar.tsx
index d651e68..72a02af 100644
--- a/src/components/ui/Sidebar.tsx
+++ b/src/components/ui/Sidebar.tsx
@@ -44,10 +44,25 @@ function UsersIcon({ active }: { active: boolean }) {
);
}
+function ActivityLogIcon({ active }: { active: boolean }) {
+ return (
+
+ );
+}
+
export function Sidebar({ showUsers }: SidebarProps) {
const pathname = usePathname();
const isServers = pathname === '/dashboard' || pathname.startsWith('/dashboard/server');
const isUsers = pathname.startsWith('/dashboard/membership');
+ const isActivityLog = pathname.startsWith('/dashboard/activity-log');
const navItems = [
{
@@ -56,6 +71,12 @@ export function Sidebar({ showUsers }: SidebarProps) {
active: isServers,
icon:
},
+ {
+ href: '/dashboard/activity-log',
+ label: 'Activity Log',
+ active: isActivityLog,
+ icon:
+ },
...(showUsers
? [
{
diff --git a/src/lib/actions/activityLog.ts b/src/lib/actions/activityLog.ts
new file mode 100644
index 0000000..efb14e3
--- /dev/null
+++ b/src/lib/actions/activityLog.ts
@@ -0,0 +1,36 @@
+'use server';
+
+import { requireAuth } from '@/lib/auth/server';
+import { getActivityLog, getGlobalActivityLog } from '@/lib/api/server/activityLog';
+import type { ActivityLog, ActivityLogFilter } from '@/lib/schemas/activityLog';
+
+export async function getActivityLogAction(
+ serverId: string,
+ filter?: ActivityLogFilter
+): Promise<{ success: true; data: ActivityLog[] } | { success: false; message: string }> {
+ try {
+ const session = await requireAuth();
+ const data = await getActivityLog(session.token!, serverId, filter);
+ return { success: true, data };
+ } catch (error) {
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Failed to fetch activity log'
+ };
+ }
+}
+
+export async function getGlobalActivityLogAction(
+ filter?: ActivityLogFilter
+): Promise<{ success: true; data: ActivityLog[] } | { success: false; message: string }> {
+ try {
+ const session = await requireAuth();
+ const data = await getGlobalActivityLog(session.token!, filter);
+ return { success: true, data };
+ } catch (error) {
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Failed to fetch activity log'
+ };
+ }
+}
diff --git a/src/lib/api/server/activityLog.ts b/src/lib/api/server/activityLog.ts
new file mode 100644
index 0000000..9a5c629
--- /dev/null
+++ b/src/lib/api/server/activityLog.ts
@@ -0,0 +1,39 @@
+import { fetchServerAPI } from './base';
+import { activityLogSchema, type ActivityLog, type ActivityLogFilter } from '@/lib/schemas/activityLog';
+
+function buildQuery(filter?: ActivityLogFilter): string {
+ if (!filter) return '';
+ const params = new URLSearchParams();
+ if (filter.page !== undefined) params.set('page', String(filter.page));
+ if (filter.page_size !== undefined) params.set('page_size', String(filter.page_size));
+ if (filter.sort_by) params.set('sort_by', filter.sort_by);
+ if (filter.sort_desc !== undefined) params.set('sort_desc', String(filter.sort_desc));
+ if (filter.user_id) params.set('user_id', filter.user_id);
+ if (filter.action) params.set('action', filter.action);
+ if (filter.start_date) params.set('start_date', filter.start_date);
+ if (filter.end_date) params.set('end_date', filter.end_date);
+ const str = params.toString();
+ return str ? `?${str}` : '';
+}
+
+export async function getActivityLog(
+ token: string,
+ serverId: string,
+ filter?: ActivityLogFilter
+): Promise
{
+ const query = buildQuery(filter);
+ const response = await fetchServerAPI(
+ `/server/${serverId}/activity-log${query}`,
+ token
+ );
+ return activityLogSchema.array().parse(response.data);
+}
+
+export async function getGlobalActivityLog(
+ token: string,
+ filter?: ActivityLogFilter
+): Promise {
+ const query = buildQuery(filter);
+ const response = await fetchServerAPI(`/activity-log${query}`, token);
+ return activityLogSchema.array().parse(response.data);
+}
diff --git a/src/lib/schemas/activityLog.ts b/src/lib/schemas/activityLog.ts
new file mode 100644
index 0000000..a692524
--- /dev/null
+++ b/src/lib/schemas/activityLog.ts
@@ -0,0 +1,52 @@
+import * as z from 'zod';
+
+export const actionTypeSchema = z.enum([
+ 'config_update',
+ 'server_start',
+ 'server_stop',
+ 'server_restart',
+ 'leaderboard_update'
+]);
+
+export type ActionType = z.infer;
+
+export const activityLogSchema = z.object({
+ id: z.string(),
+ serverId: z.string(),
+ userId: z.string(),
+ username: z.string(),
+ action: actionTypeSchema,
+ details: z.string(),
+ createdAt: z.string()
+});
+
+export type ActivityLog = z.infer;
+
+export const lastActivityInfoSchema = z.object({
+ updatedAt: z.string(),
+ username: z.string(),
+ action: actionTypeSchema
+});
+
+export type LastActivityInfo = z.infer;
+
+export const activityLogFilterSchema = z.object({
+ page: z.number().optional(),
+ page_size: z.number().optional(),
+ sort_by: z.string().optional(),
+ sort_desc: z.boolean().optional(),
+ user_id: z.string().optional(),
+ action: actionTypeSchema.optional(),
+ start_date: z.string().optional(),
+ end_date: z.string().optional()
+});
+
+export type ActivityLogFilter = z.infer;
+
+export const ACTION_TYPE_LABELS: Record = {
+ config_update: 'Config Update',
+ server_start: 'Server Start',
+ server_stop: 'Server Stop',
+ server_restart: 'Server Restart',
+ leaderboard_update: 'Leaderboard Update'
+};
diff --git a/src/lib/schemas/config.ts b/src/lib/schemas/config.ts
index a5c1cd5..7046ad5 100755
--- a/src/lib/schemas/config.ts
+++ b/src/lib/schemas/config.ts
@@ -16,7 +16,8 @@ export enum ServerTab {
event = 'event',
eventRules = 'eventRules',
settings = 'settings',
- leaderboard = 'leaderboard'
+ leaderboard = 'leaderboard',
+ activityLog = 'activityLog'
}
export const serverTabSchema = z.enum(ServerTab);
diff --git a/src/lib/schemas/index.ts b/src/lib/schemas/index.ts
index fd9100e..b6011e1 100755
--- a/src/lib/schemas/index.ts
+++ b/src/lib/schemas/index.ts
@@ -4,3 +4,4 @@ export * from './config';
export * from './statistics';
export * from './lookups';
export * from './leaderboard';
+export * from './activityLog';
diff --git a/src/lib/schemas/server.ts b/src/lib/schemas/server.ts
index 6191979..5408c32 100755
--- a/src/lib/schemas/server.ts
+++ b/src/lib/schemas/server.ts
@@ -1,4 +1,5 @@
import * as z from 'zod';
+import { lastActivityInfoSchema } from './activityLog';
export enum ServiceStatus {
Unknown,
@@ -57,7 +58,8 @@ export const serverSchema = z.object({
id: z.uuid(),
name: z.string().min(1),
status: z.enum(ServiceStatus),
- state: stateSchema.optional().nullable()
+ state: stateSchema.optional().nullable(),
+ lastActivity: lastActivityInfoSchema.optional().nullable()
});
export type Server = z.infer;