diff --git a/src/components/server/ServerConfigurationTabs.tsx b/src/components/server/ServerConfigurationTabs.tsx
index a8f1229..8a06b9f 100755
--- a/src/components/server/ServerConfigurationTabs.tsx
+++ b/src/components/server/ServerConfigurationTabs.tsx
@@ -9,6 +9,7 @@ import { ServerSettingsEditor } from '@/components/configuration/ServerSettingsE
import { StatisticsDashboard } from '@/components/statistics/StatisticsDashboard';
import { LeaderboardManager } from '@/components/leaderboard/LeaderboardManager';
import { ActivityLogTable } from '@/components/activityLog/ActivityLogTable';
+import { ServerLogViewer } from '@/components/server/ServerLogViewer';
import { useState } from 'react';
import { StateHistoryStats } from '@/lib/schemas';
import { Leaderboard } from '@/lib/schemas/leaderboard';
@@ -28,7 +29,8 @@ const tabs = [
{ id: ServerTab.eventRules, name: 'Event Rules', icon: 'π' },
{ id: ServerTab.settings, name: 'Server Settings', icon: 'π§' },
{ id: ServerTab.leaderboard, name: 'Leaderboard', icon: 'π' },
- { id: ServerTab.activityLog, name: 'History', icon: 'π' }
+ { id: ServerTab.activityLog, name: 'History', icon: 'π' },
+ { id: ServerTab.logs, name: 'Logs', icon: 'π' }
];
const configTabs = new Set([
@@ -143,6 +145,9 @@ export function ServerConfigurationTabs({
case ServerTab.activityLog:
return ;
+ case ServerTab.logs:
+ return ;
+
default:
return (
diff --git a/src/components/server/ServerLogViewer.tsx b/src/components/server/ServerLogViewer.tsx
new file mode 100644
index 0000000..978ac5b
--- /dev/null
+++ b/src/components/server/ServerLogViewer.tsx
@@ -0,0 +1,153 @@
+'use client';
+
+import { useEffect, useRef, useState, useCallback } from 'react';
+import { useWebSocket } from '@/lib/websocket/context';
+import type { WebSocketMessage } from '@/lib/schemas/websocket';
+import { getServerLogAction } from '@/lib/actions/serverLog';
+
+const MAX_BUFFER = 5000;
+const LINE_OPTIONS = [100, 500, 1000, 5000] as const;
+
+interface ServerLogViewerProps {
+ serverId: string;
+}
+
+export function ServerLogViewer({ serverId }: ServerLogViewerProps) {
+ const [lines, setLines] = useState
([]);
+ const [initialLines, setInitialLines] = useState(100);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [isPaused, setIsPaused] = useState(false);
+
+ const { associateWithServer, addMessageHandler, removeMessageHandler } = useWebSocket();
+ const scrollRef = useRef(null);
+ const isPausedRef = useRef(false);
+
+ const appendLines = useCallback((newLines: string[]) => {
+ setLines((prev) => {
+ const combined = [...prev, ...newLines];
+ return combined.length > MAX_BUFFER ? combined.slice(combined.length - MAX_BUFFER) : combined;
+ });
+ }, []);
+
+ const fetchInitial = useCallback(
+ async (count: number) => {
+ setIsLoading(true);
+ setError(null);
+ const result = await getServerLogAction(serverId, count);
+ setIsLoading(false);
+ if (!result.success) {
+ setError(result.message);
+ return;
+ }
+ setLines(result.lines);
+ },
+ [serverId]
+ );
+
+ useEffect(() => {
+ associateWithServer(serverId);
+ fetchInitial(initialLines);
+ }, [serverId, associateWithServer, fetchInitial, initialLines]);
+
+ const handleMessage = useCallback(
+ (message: WebSocketMessage) => {
+ if (message.type !== 'log_line') return;
+ if (message.server_id !== serverId) return;
+ appendLines([message.data.line]);
+ },
+ [serverId, appendLines]
+ );
+
+ useEffect(() => {
+ addMessageHandler(handleMessage);
+ return () => {
+ removeMessageHandler(handleMessage);
+ };
+ }, [addMessageHandler, removeMessageHandler, handleMessage]);
+
+ // Auto-scroll to bottom when new lines arrive, unless paused
+ useEffect(() => {
+ if (!isPausedRef.current && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [lines]);
+
+ const handleScroll = () => {
+ if (!scrollRef.current) return;
+ const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < 40;
+ const shouldPause = !isAtBottom;
+ isPausedRef.current = shouldPause;
+ setIsPaused(shouldPause);
+ };
+
+ const jumpToBottom = () => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ isPausedRef.current = false;
+ setIsPaused(false);
+ };
+
+ const handleLineLimitChange = (e: React.ChangeEvent) => {
+ const count = Number(e.target.value);
+ setInitialLines(count);
+ };
+
+ return (
+
+
+
+
+ {lines.length} lines buffered
+
+
+ {error &&
{error}
}
+
+
+
+ {isLoading ? (
+
Loadingβ¦
+ ) : lines.length === 0 ? (
+
No log output yet.
+ ) : (
+ lines.map((line, i) => (
+
+ {line}
+
+ ))
+ )}
+
+
+ {isPaused && (
+
+
+ Paused
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/lib/actions/serverLog.ts b/src/lib/actions/serverLog.ts
new file mode 100644
index 0000000..c50ce1b
--- /dev/null
+++ b/src/lib/actions/serverLog.ts
@@ -0,0 +1,20 @@
+'use server';
+
+import { requireAuth } from '@/lib/auth/server';
+import { getServerLog } from '@/lib/api/server/serverLog';
+
+export async function getServerLogAction(
+ serverId: string,
+ lines: number = 100
+): Promise<{ success: true; lines: string[] } | { success: false; message: string }> {
+ try {
+ const session = await requireAuth();
+ const data = await getServerLog(session.token!, serverId, lines);
+ return { success: true, lines: data };
+ } catch (error) {
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Failed to fetch server log'
+ };
+ }
+}
diff --git a/src/lib/api/server/serverLog.ts b/src/lib/api/server/serverLog.ts
new file mode 100644
index 0000000..584425a
--- /dev/null
+++ b/src/lib/api/server/serverLog.ts
@@ -0,0 +1,13 @@
+import { fetchServerAPI } from './base';
+
+export async function getServerLog(
+ token: string,
+ serverId: string,
+ lines: number = 100
+): Promise {
+ const response = await fetchServerAPI<{ lines: string[] }>(
+ `/server/${serverId}/log?lines=${lines}`,
+ token
+ );
+ return response.data?.lines ?? [];
+}
diff --git a/src/lib/schemas/config.ts b/src/lib/schemas/config.ts
index 7046ad5..171d9bd 100755
--- a/src/lib/schemas/config.ts
+++ b/src/lib/schemas/config.ts
@@ -17,7 +17,8 @@ export enum ServerTab {
eventRules = 'eventRules',
settings = 'settings',
leaderboard = 'leaderboard',
- activityLog = 'activityLog'
+ activityLog = 'activityLog',
+ logs = 'logs'
}
export const serverTabSchema = z.enum(ServerTab);
diff --git a/src/lib/schemas/websocket.ts b/src/lib/schemas/websocket.ts
index 44a11a1..789c5b9 100755
--- a/src/lib/schemas/websocket.ts
+++ b/src/lib/schemas/websocket.ts
@@ -44,6 +44,13 @@ export const completeDataSchema = z.object({
export type CompleteData = z.infer;
+// Log line data schema
+export const logLineDataSchema = z.object({
+ line: z.string()
+});
+
+export type LogLineData = z.infer;
+
// WebSocket message schema using discriminated union
export const webSocketMessageSchema = z.discriminatedUnion('type', [
z.object({
@@ -69,6 +76,12 @@ export const webSocketMessageSchema = z.discriminatedUnion('type', [
server_id: z.string(),
timestamp: z.number(),
data: completeDataSchema
+ }),
+ z.object({
+ type: z.literal('log_line'),
+ server_id: z.string(),
+ timestamp: z.number(),
+ data: logLineDataSchema
})
]);