diff --git a/pom.xml b/pom.xml index 05664e7..0e9d1a4 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,21 @@ jspecify 1.0.0 + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + com.google.code.gson + gson + 2.11.0 + + + io.javalin + javalin-bundle + 6.1.3 + diff --git a/src/main/java/io/github/kinsleykajiva/Main.java b/src/main/java/io/github/kinsleykajiva/Main.java index 764d804..a677c43 100644 --- a/src/main/java/io/github/kinsleykajiva/Main.java +++ b/src/main/java/io/github/kinsleykajiva/Main.java @@ -1,252 +1,67 @@ package io.github.kinsleykajiva; -import io.github.kinsleykajiva.janus.JanusClient; -import io.github.kinsleykajiva.janus.JanusConfiguration; -import io.github.kinsleykajiva.janus.JanusSession; -import io.github.kinsleykajiva.janus.ServerInfo; -import io.github.kinsleykajiva.janus.admin.JanusAdminClient; +import io.github.kinsleykajiva.admin_ui.JanusAdminUI; import io.github.kinsleykajiva.janus.admin.JanusAdminConfiguration; -import io.github.kinsleykajiva.janus.admin.messages.ListSessionsResponse; -import io.github.kinsleykajiva.janus.handle.impl.VideoRoomHandle; -import io.github.kinsleykajiva.janus.plugins.videoroom.events.*; -import io.github.kinsleykajiva.janus.plugins.videoroom.listeners.JanusVideoRoomListener; -import io.github.kinsleykajiva.janus.plugins.videoroom.models.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; -import java.util.concurrent.TimeUnit; public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { - // Enable debug logging for more details + // Enable info logging for more details System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "INFO"); - // Configure the Janus client - JanusConfiguration config = new JanusConfiguration( - "localhost", // Replace with your Janus server IP - 8188, - "/janus", - false, - true - ); - + // 1. Configure the Janus Admin client + // Replace with your Janus admin websocket URI and secret JanusAdminConfiguration adminConfig = new JanusAdminConfiguration( URI.create("ws://localhost:7188/janus"), "janusoverlord" ); - JanusClient client = new JanusClient(config); - - // Add shutdown hook to ensure proper cleanup - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - logger.info("Shutting down JanusClient..."); - client.disconnect(); - })); - try { - // 1. Connect to Janus - logger.info("Connecting to Janus server at {}...", config.getUri()); - client.connect().get(10, TimeUnit.SECONDS); - - // 2. Get Server Info - ServerInfo serverInfo = client.getServerInfo().get(); - logger.info("Server Info: Janus v{}", serverInfo.versionString()); - - // 3. Create a Session - logger.info("Creating Janus session..."); - JanusSession session = client.createSession().get(); - logger.info("Session created with ID: {}", session.getSessionId()); - - // Run the VideoRoom example - // runVideoRoomExample(session); - - // Run the Admin Client Example - runAdminClientExample(adminConfig); - - - // Keep the application running to listen for more events - logger.info("Example finished. Application will exit in 10 seconds."); - Thread.sleep(10000); + // 2. Initialize the Admin UI feature + // This will start caching admin events and prepare the web server. + // Using default constructor: + // - Cache folder: 'cache/' in the application's root directory. + // - Web UI port: A random available port. + // - Authentication: Disabled by default. + logger.info("Initializing Janus Admin UI..."); + + // To enable authentication, provide a username and password + final String username = "admin"; + final String password = "password"; + logger.info("Admin UI credentials: username='{}', password='{}'", username, password); + final JanusAdminUI adminUI = new JanusAdminUI(adminConfig, "cache", 0, username, password); + + // To run without authentication: + // final JanusAdminUI adminUI = new JanusAdminUI(adminConfig); + + + // 3. Add a shutdown hook for graceful exit + // This ensures the web server and client are disconnected properly. + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.info("Shutting down Janus Admin UI..."); + adminUI.stop(); + })); + + // 4. Start the UI server + // The server will run in the background. + adminUI.start(); + logger.info("Admin UI server has been started. Check the console logs above for the exact port number."); + logger.info("You can now access the UI in your browser."); + logger.info("Press Enter in this console to stop the application."); + + // 5. Keep the application running + // The web server is on a background thread, so we need to prevent the main thread from exiting. + System.in.read(); } catch (Exception e) { - logger.error("An error occurred: {}", e.getMessage(), e); + logger.error("An error occurred during application startup: {}", e.getMessage(), e); } finally { - logger.info("Disconnecting client."); - client.disconnect(); - } - } - - /** - * A comprehensive example demonstrating the VideoRoomHandle workflow. - * @param session An active JanusSession. - * @throws Exception if any operation fails. - */ - public static void runVideoRoomExample(JanusSession session) throws Exception { - System.out.println("\n--- Running VideoRoom Example ---\n"); - - // 1. Attach to the VideoRoom Plugin - // This creates a handle, which is a context for all subsequent plugin interactions. - System.out.println("Attaching to VideoRoom plugin..."); - VideoRoomHandle videoRoomHandle = session.attachToVideoRoom().get(); - System.out.println("VideoRoom handle attached with ID: " + videoRoomHandle.getHandleId()); - - // 2. Add a listener for VideoRoom events - // This is crucial for handling asynchronous responses from the plugin. - videoRoomHandle.addVideoRoomListener(new JanusVideoRoomListener() { - @Override - public void onJoined(JoinedEvent event) { - System.out.printf(">>> Successfully joined room %d as a publisher (My ID: %d)%n", - event.room(), event.id()); - if (event.publishers().isEmpty()) { - System.out.println(">>> There are no other active publishers in the room."); - } else { - event.publishers().forEach(publisher -> - System.out.printf(">>> Active publisher in room: %s (ID: %d)%n", - publisher.display(), publisher.id())); - } - } - - - - - - @Override - public void onPublisherAdded(PublisherAddedEvent event) { - event.publishers().forEach(publisher -> - System.out.printf(">>> EVENT: A new publisher has entered the room: %s (ID: %d)%n", - publisher.display(), publisher.id())); - } - - @Override - public void onUnpublished(UnpublishedEvent event) { - System.out.println(">>> EVENT: Publisher " + event.unpublished() + " has unpublished their stream."); - } - - @Override - public void onParticipantLeft(ParticipantLeftEvent event) { - System.out.println(">>> EVENT: Participant " + event.leaving() + " has left the room."); - } - - @Override - public void onRoomDestroyed(RoomDestroyedEvent event) { - System.out.println(">>> EVENT: Room " + event.room() + " has been destroyed."); - } - }); - - // 3. Create a new room - System.out.println("Creating a new video room..."); - CreateRoomRequest createRequest = new CreateRoomRequest.Builder() - .setDescription("My Java SDK Test Room") - .setPublishers(6) - .build(); - CreateRoomResponse createResponse = videoRoomHandle.createRoom(createRequest).get(); - final long roomId = createResponse.room(); - System.out.println("Room created with ID: " + roomId); - Thread.sleep(500); // Pause for clarity - - // 4. Join the room as a publisher - System.out.println("Joining room " + roomId + " as a publisher......"); - JoinRoomRequest joinRequest = new JoinRoomRequest.Builder(roomId) - .setDisplay("JavaSDKUser") - .build(); - videoRoomHandle.join(joinRequest).get(); // join() is async, the onJoined event will fire - - // Give Janus time to process and send the 'joined' event - Thread.sleep(1000); - - // 5. List participants (should include us) - System.out.println("Listing participants in room " + roomId + "..."); - ListParticipantsRequest listRequest = new ListParticipantsRequest(roomId); - ListParticipantsResponse listResponse = videoRoomHandle.listParticipants(listRequest).get(); - System.out.println("Participants found: " + listResponse.participants().size()); - listResponse.participants().forEach(p -> - System.out.printf(" - Participant ID: %d, Display: '%s', Publisher: %b%n", - p.id(), p.display(), p.publisher())); - Thread.sleep(500); - - // 6. Conceptually "publish" a stream. - // In a real application, this would involve sending a JSEP offer from a WebRTC client. - // The handle's `sendMessage(body, jsep)` would be used. For this example, we just - // simulate the action and then unpublish. - System.out.println("Simulating publishing a stream... (In a real app, this sends a JSEP offer)"); - // An `onPublisherAdded` event would be sent to all participants after this. - Thread.sleep(1000); - - // 7. Unpublish the stream - System.out.println("Unpublishing the stream..."); - // In this example, we comment out the unpublish call. - // The Janus plugin would return an error "Can't unpublish, not published" - // because we never sent a real `publish` request with a JSEP offer. - // videoRoomHandle.unpublish().get(); - Thread.sleep(500); - - // 8. Leave the room - System.out.println("Leaving the room..."); - videoRoomHandle.leave().get(); // An `onParticipantLeft` event will fire for others - Thread.sleep(500); - - // 9. Destroy the room - System.out.println("Destroying room " + roomId + "..."); - DestroyRoomRequest destroyRequest = new DestroyRoomRequest(roomId, null, true); - videoRoomHandle.destroyRoom(destroyRequest).get(); // An `onRoomDestroyed` event will fire for the handle - - System.out.println("\n--- VideoRoom Example Finished ---\n"); - } - - public static void runAdminClientExample(JanusAdminConfiguration adminConfig) throws Exception { - System.out.println("\n--- Running Admin Client Example ---\n"); - - JanusAdminClient adminClient = new JanusAdminClient(adminConfig); - - adminClient.getAdminMonitor().addListener(event -> { - System.out.println(">>> ADMIN EVENT: " + event.toString(2)); - }); - - System.out.println("Pinging admin endpoint..."); - adminClient.ping().thenAccept(response -> { - System.out.println("Ping response: " + response.toString(2)); - }).get(); - - System.out.println("Getting server info..."); - adminClient.info().thenAccept(info -> { - System.out.println("Server info: " + info.versionString()); - }).get(); - - System.out.println("Getting status..."); - adminClient.getStatus().thenAccept(status -> { - System.out.println("Status: " + status.toString(2)); - }).get(); - - System.out.println("Listing active sessions..."); - ListSessionsResponse sessionsResponse = adminClient.listSessions().get(); - System.out.println("Active sessions: " + sessionsResponse.getSessionIds()); - - if (!sessionsResponse.getSessionIds().isEmpty()) { - long firstSessionId = sessionsResponse.getSessionIds().get(0); - System.out.println("Listing handles for session: " + firstSessionId); - adminClient.listHandles(firstSessionId).thenAccept(handles -> { - System.out.println("Handles: " + handles.getHandleIds()); - }).get(); + logger.info("Application shutting down."); } - - System.out.println("Setting log level to 4..."); - adminClient.setLogLevel(4).get(); - System.out.println("Log level set."); - - System.out.println("Getting status again..."); - adminClient.getStatus().thenAccept(status -> { - System.out.println("Status: " + status.toString(2)); - }).get(); - - System.out.println("Setting log level back to 3..."); - adminClient.setLogLevel(3).get(); - System.out.println("Log level set."); - - - adminClient.disconnect(); - System.out.println("\n--- Admin Client Example Finished ---\n"); } } \ No newline at end of file diff --git a/src/main/java/io/github/kinsleykajiva/admin_ui/AdminWebApp.java b/src/main/java/io/github/kinsleykajiva/admin_ui/AdminWebApp.java new file mode 100644 index 0000000..dcb7b26 --- /dev/null +++ b/src/main/java/io/github/kinsleykajiva/admin_ui/AdminWebApp.java @@ -0,0 +1,145 @@ +package io.github.kinsleykajiva.admin_ui; + +import com.google.gson.Gson; +import io.github.kinsleykajiva.cache.CacheService; +import io.javalin.Javalin; +import io.javalin.json.JsonMapper; +import io.javalin.websocket.WsContext; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class AdminWebApp { + + private final Javalin app; + private final Set wsClients = Collections.synchronizedSet(new HashSet<>()); + + public AdminWebApp(int port, CacheService cacheService, AuthService authService) { + + // Configure Gson for JSON mapping + Gson gson = new Gson(); + JsonMapper gsonMapper = new JsonMapper() { + @Override + public String toJsonString(Object obj, Type type) { + return gson.toJson(obj, type); + } + + @Override + public T fromJsonString(String json, Type type) { + return gson.fromJson(json, type); + } + }; + + // Create Javalin app + this.app = Javalin.create(config -> { + config.jsonMapper(gsonMapper); + config.staticFiles.add(staticFiles -> { + staticFiles.hostedPath = "/"; + staticFiles.directory = "/admin_ui"; + staticFiles.precompress = false; // Disable for simplicity + }); + }); + + // Authentication Filter (Before Handler) + app.before(ctx -> { + if (authService == null) { + return; // No auth configured, allow all + } + String path = ctx.path(); + if (path.equals("/login.html") || path.startsWith("/css/") || path.startsWith("/js/") || path.equals("/api/login")) { + return; // Allow access to login page and its assets + } + + String token = ctx.cookie("session_token"); + if (!authService.validateToken(token)) { + ctx.redirect("/login.html"); + ctx.status(401); + } + }); + + // API Handlers + app.post("/api/login", ctx -> { + if (authService == null) { + ctx.status(500).json(Map.of("error", "Authentication not configured")); + return; + } + var user = ctx.bodyAsClass(Map.class); + String token = authService.login((String) user.get("username"), (String) user.get("password")); + if (token != null) { + ctx.cookie("session_token", token); + ctx.json(Map.of("token", token)); + } else { + ctx.status(401).json(Map.of("error", "Invalid credentials")); + } + }); + + app.post("/api/logout", ctx -> { + if (authService != null) { + authService.logout(ctx.cookie("session_token")); + } + ctx.removeCookie("session_token"); + ctx.json(Map.of("message", "Logged out")); + }); + + app.get("/api/events", ctx -> { + ctx.json(cacheService.getEvents()); + }); + + app.get("/api/history", ctx -> { + ctx.json(cacheService.getEvents()); + }); + + app.post("/api/events/clear", ctx -> { + cacheService.clearCache(); + ctx.json(Map.of("message", "Cache cleared")); + }); + + // WebSocket Handler + app.ws("/ws", ws -> { + ws.onConnect(ctx -> { + if (authService != null) { + String token = ctx.queryParam("token"); + if (!authService.validateToken(token)) { + ctx.closeSession(1008, "Unauthorized"); // 1008 = Policy Violation + return; + } + } + ctx.enableAutomaticPings(); + wsClients.add(ctx); + }); + ws.onClose(ctx -> { + wsClients.remove(ctx); + }); + }); + + // Start server + if (port > 0) { + this.app.start(port); + } else { + this.app.start(0); // start on random port + } + } + + public void start() { + System.out.println("Admin Web App started on port " + app.port()); + } + + public int getPort() { + return app.port(); + } + + public void stop() { + app.stop(); + } + + public void broadcast(String message) { + wsClients.forEach(client -> { + if (client.session.isOpen()) { + client.send(message); + } + }); + } +} diff --git a/src/main/java/io/github/kinsleykajiva/admin_ui/AuthService.java b/src/main/java/io/github/kinsleykajiva/admin_ui/AuthService.java new file mode 100644 index 0000000..3526a6b --- /dev/null +++ b/src/main/java/io/github/kinsleykajiva/admin_ui/AuthService.java @@ -0,0 +1,39 @@ +package io.github.kinsleykajiva.admin_ui; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class AuthService { + + private final String username; + private final String password; + private final Map activeSessions = new ConcurrentHashMap<>(); // Token -> Username + + public AuthService(String username, String password) { + if (username == null || username.isBlank() || password == null || password.isBlank()) { + throw new IllegalArgumentException("Username and password cannot be null or empty."); + } + this.username = username; + this.password = password; + } + + public String login(String username, String password) { + if (this.username.equals(username) && this.password.equals(password)) { + String token = UUID.randomUUID().toString(); + activeSessions.put(token, username); + return token; + } + return null; + } + + public void logout(String token) { + if (token != null) { + activeSessions.remove(token); + } + } + + public boolean validateToken(String token) { + return token != null && activeSessions.containsKey(token); + } +} diff --git a/src/main/java/io/github/kinsleykajiva/admin_ui/JanusAdminUI.java b/src/main/java/io/github/kinsleykajiva/admin_ui/JanusAdminUI.java new file mode 100644 index 0000000..d581bf7 --- /dev/null +++ b/src/main/java/io/github/kinsleykajiva/admin_ui/JanusAdminUI.java @@ -0,0 +1,46 @@ +package io.github.kinsleykajiva.admin_ui; + +import io.github.kinsleykajiva.cache.CacheService; +import io.github.kinsleykajiva.janus.admin.JanusAdminClient; +import io.github.kinsleykajiva.janus.admin.JanusAdminConfiguration; + +import java.io.IOException; + +public class JanusAdminUI { + private final AdminWebApp adminWebApp; + private final JanusAdminClient janusAdminClient; + private final CacheService cacheService; + private final AuthService authService; + + public JanusAdminUI(JanusAdminConfiguration adminConfig) throws IOException { + this(adminConfig, "cache", 0, null, null); + } + + public JanusAdminUI(JanusAdminConfiguration adminConfig, String cachePath, int uiPort, String username, String password) throws IOException { + this.cacheService = new CacheService(cachePath); + if (username != null && password != null) { + this.authService = new AuthService(username, password); + } else { + this.authService = null; + } + this.adminWebApp = new AdminWebApp(uiPort, this.cacheService, this.authService); + this.janusAdminClient = new JanusAdminClient(adminConfig, this.cacheService); + + this.janusAdminClient.getAdminMonitor().addListener(event -> { + adminWebApp.broadcast(event.toString()); + }); + } + + public void start() { + adminWebApp.start(); + } + + public void stop() { + adminWebApp.stop(); + janusAdminClient.disconnect(); + } + + public JanusAdminClient getJanusAdminClient() { + return janusAdminClient; + } +} diff --git a/src/main/java/io/github/kinsleykajiva/cache/CacheService.java b/src/main/java/io/github/kinsleykajiva/cache/CacheService.java new file mode 100644 index 0000000..2793bd1 --- /dev/null +++ b/src/main/java/io/github/kinsleykajiva/cache/CacheService.java @@ -0,0 +1,84 @@ +package io.github.kinsleykajiva.cache; + +import com.google.gson.Gson; +import java.io.File; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.json.JSONObject; + +public class CacheService { + private final String dbUrl; + private final Gson gson = new Gson(); + + public CacheService(String cachePath) { + File cacheDir = new File(cachePath); + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + this.dbUrl = "jdbc:sqlite:" + cachePath + "/admin-event.sql3"; + initDb(); + } + + private Connection connect() throws SQLException { + return DriverManager.getConnection(dbUrl); + } + + private void initDb() { + String sql = "CREATE TABLE IF NOT EXISTS admin_events (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP," + + "event_data TEXT NOT NULL" + + ");"; + + try (Connection conn = this.connect(); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } catch (SQLException e) { + System.out.println(e.getMessage()); + } + } + + public void addEvent(JSONObject event) { + String sql = "INSERT INTO admin_events(event_data) VALUES(?)"; + + try (Connection conn = this.connect(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, event.toString()); + pstmt.executeUpdate(); + } catch (SQLException e) { + System.out.println(e.getMessage()); + } + } + + public List> getEvents() { + String sql = "SELECT id, timestamp, event_data FROM admin_events ORDER BY timestamp DESC"; + List> eventsList = new ArrayList<>(); + try (Connection conn = this.connect(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + Map eventObject = new java.util.HashMap<>(); + eventObject.put("id", rs.getInt("id")); + eventObject.put("timestamp", rs.getString("timestamp")); + eventObject.put("event_data", gson.fromJson(rs.getString("event_data"), Map.class)); + eventsList.add(eventObject); + } + } catch (SQLException e) { + System.out.println(e.getMessage()); + } + return eventsList; + } + + public void clearCache() { + String sql = "DELETE FROM admin_events"; + try (Connection conn = this.connect(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.executeUpdate(); + } catch (SQLException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/main/java/io/github/kinsleykajiva/janus/admin/JanusAdminClient.java b/src/main/java/io/github/kinsleykajiva/janus/admin/JanusAdminClient.java index fa122e6..9fd6bb0 100644 --- a/src/main/java/io/github/kinsleykajiva/janus/admin/JanusAdminClient.java +++ b/src/main/java/io/github/kinsleykajiva/janus/admin/JanusAdminClient.java @@ -30,6 +30,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; +import io.github.kinsleykajiva.cache.CacheService; import org.slf4j.LoggerFactory; import java.net.http.HttpClient; @@ -47,13 +48,19 @@ public class JanusAdminClient implements WebSocket.Listener { private final TransactionManager transactionManager; private final StringBuilder messageBuffer = new StringBuilder(); private final JanusAdminMonitor adminMonitor; + private final CacheService cacheService; public JanusAdminClient(JanusAdminConfiguration config) { + this(config, null); + } + + public JanusAdminClient(JanusAdminConfiguration config, CacheService cacheService) { this.config = config; this.transactionManager = new TransactionManager(); this.executor = Executors.newVirtualThreadPerTaskExecutor(); this.httpClient = HttpClient.newBuilder().executor(this.executor).build(); this.adminMonitor = new JanusAdminMonitor(); + this.cacheService = cacheService; try { logger.info("Starting admin connection attempt..."); @@ -126,6 +133,9 @@ private void processMessage(String message) { if (transactionId != null && !transactionId.isEmpty()) { transactionManager.completeTransaction(transactionId, json); } else { + if (cacheService != null) { + cacheService.addEvent(json); + } adminMonitor.dispatchEvent(json); } } catch (JSONException e) { diff --git a/src/main/resources/admin_ui/history.html b/src/main/resources/admin_ui/history.html new file mode 100644 index 0000000..9322dac --- /dev/null +++ b/src/main/resources/admin_ui/history.html @@ -0,0 +1,83 @@ + + + + + + Janus Admin - History + + + + + + + + + + + + +
+
+

History & Analytics

+
+ +
+
+ Event Analytics +
+
+

This section will contain charts and filters for analyzing historical event data.

+ +
+
+
+ + + + + + + + + + diff --git a/src/main/resources/admin_ui/index.html b/src/main/resources/admin_ui/index.html new file mode 100644 index 0000000..33485dc --- /dev/null +++ b/src/main/resources/admin_ui/index.html @@ -0,0 +1,82 @@ + + + + + + Janus Admin - Dashboard + + + + + + + + + + + + +
+
+

Dashboard

+
+ +
+
+ Live Admin Events +
+
+
+ +
+
+
+
+ + + + + + + + diff --git a/src/main/resources/admin_ui/js/app.js b/src/main/resources/admin_ui/js/app.js new file mode 100644 index 0000000..4cb4656 --- /dev/null +++ b/src/main/resources/admin_ui/js/app.js @@ -0,0 +1,28 @@ +document.addEventListener('DOMContentLoaded', () => { + const logoutButton = document.getElementById('logout-button'); + + if (logoutButton) { + logoutButton.addEventListener('click', async () => { + try { + const response = await fetch('/api/logout', { + method: 'POST' + }); + + if (response.ok) { + window.location.href = '/login.html'; + } else { + console.error('Logout failed'); + } + } catch (error) { + console.error('Error during logout:', error); + } + }); + } + + // A simple way to get a cookie + function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + } +}); diff --git a/src/main/resources/admin_ui/js/dashboard.js b/src/main/resources/admin_ui/js/dashboard.js new file mode 100644 index 0000000..90a6d65 --- /dev/null +++ b/src/main/resources/admin_ui/js/dashboard.js @@ -0,0 +1,63 @@ +document.addEventListener('DOMContentLoaded', () => { + const liveEventsLog = document.getElementById('live-events-log'); + + function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + } + + const token = getCookie('session_token'); + + if (!token) { + console.error('No session token found. Redirecting to login.'); + window.location.href = '/login.html'; + return; + } + + if (liveEventsLog) { + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${wsProtocol}//${window.location.host}/ws?token=${token}`; + + const socket = new WebSocket(wsUrl); + + socket.onopen = () => { + console.log('WebSocket connection established.'); + const logEntry = document.createElement('div'); + logEntry.textContent = 'Connection established. Waiting for events...'; + liveEventsLog.appendChild(logEntry); + }; + + socket.onmessage = (event) => { + const logEntry = document.createElement('pre'); + try { + const eventData = JSON.parse(event.data); + logEntry.textContent = JSON.stringify(eventData, null, 2); + } catch (e) { + logEntry.textContent = event.data; + } + liveEventsLog.appendChild(logEntry); + // Scroll to the bottom + liveEventsLog.scrollTop = liveEventsLog.scrollHeight; + }; + + socket.onclose = (event) => { + console.log('WebSocket connection closed.', event); + const logEntry = document.createElement('div'); + logEntry.textContent = `Connection closed. Code: ${event.code}, Reason: ${event.reason}`; + logEntry.style.color = 'red'; + liveEventsLog.appendChild(logEntry); + if (event.code === 401) { + window.location.href = '/login.html'; + } + }; + + socket.onerror = (error) => { + console.error('WebSocket error:', error); + const logEntry = document.createElement('div'); + logEntry.textContent = 'An error occurred with the WebSocket connection.'; + logEntry.style.color = 'red'; + liveEventsLog.appendChild(logEntry); + }; + } +}); diff --git a/src/main/resources/admin_ui/js/history.js b/src/main/resources/admin_ui/js/history.js new file mode 100644 index 0000000..cad502b --- /dev/null +++ b/src/main/resources/admin_ui/js/history.js @@ -0,0 +1,12 @@ +document.addEventListener('DOMContentLoaded', () => { + console.log('History page loaded. Charting functionality will be implemented here.'); + + // Placeholder for fetching and rendering historical data + async function loadHistory() { + // const response = await fetch('/api/history?range=7d'); + // const data = await response.json(); + // renderChart(data); + } + + // loadHistory(); +}); diff --git a/src/main/resources/admin_ui/js/login.js b/src/main/resources/admin_ui/js/login.js new file mode 100644 index 0000000..8b31a4d --- /dev/null +++ b/src/main/resources/admin_ui/js/login.js @@ -0,0 +1,35 @@ +document.addEventListener('DOMContentLoaded', () => { + const loginForm = document.getElementById('login-form'); + const errorMessage = document.getElementById('error-message'); + + if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + errorMessage.classList.add('d-none'); + + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + + try { + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }); + + if (response.ok) { + window.location.href = '/index.html'; + } else { + const errorData = await response.json(); + errorMessage.textContent = errorData.error || 'Login failed. Please try again.'; + errorMessage.classList.remove('d-none'); + } + } catch (error) { + errorMessage.textContent = 'An error occurred. Please try again.'; + errorMessage.classList.remove('d-none'); + } + }); + } +}); diff --git a/src/main/resources/admin_ui/login.html b/src/main/resources/admin_ui/login.html new file mode 100644 index 0000000..82cb29b --- /dev/null +++ b/src/main/resources/admin_ui/login.html @@ -0,0 +1,67 @@ + + + + + + Janus Admin - Login + + + + + + + + + +
+
+
+
+
+
+

Janus Admin UI

+
+
+
+
+
+
+
+

Sign in to the platform

+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + diff --git a/src/main/resources/admin_ui/static/app.js b/src/main/resources/admin_ui/static/app.js new file mode 100644 index 0000000..0a18ccd --- /dev/null +++ b/src/main/resources/admin_ui/static/app.js @@ -0,0 +1,84 @@ +document.addEventListener('DOMContentLoaded', function() { + const eventsTableBody = document.getElementById('eventsTableBody'); + const clearCacheBtn = document.getElementById('clearCacheBtn'); + const ctx = document.getElementById('eventsChart').getContext('2d'); + let eventsChart; + + function fetchEvents() { + fetch('/api/events') + .then(response => response.json()) + .then(data => { + renderTable(data); + renderChart(data); + }) + .catch(error => console.error('Error fetching events:', error)); + } + + function renderTable(events) { + eventsTableBody.innerHTML = ''; + if (events.length === 0) { + eventsTableBody.innerHTML = 'No events found.'; + return; + } + events.forEach(event => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${event.id} + ${event.timestamp} +
${JSON.stringify(event.event_data, null, 2)}
+ `; + eventsTableBody.appendChild(row); + }); + } + + function renderChart(events) { + const labels = events.map(e => new Date(e.timestamp).toLocaleTimeString()); + const data = events.reduce((acc, event) => { + const type = event.event_data.type; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + + if (eventsChart) { + eventsChart.destroy(); + } + + eventsChart = new Chart(ctx, { + type: 'bar', + data: { + labels: Object.keys(data), + datasets: [{ + label: '# of Events', + data: Object.values(data), + backgroundColor: 'rgba(75, 192, 192, 0.2)', + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 1 + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + } + + + clearCacheBtn.addEventListener('click', function() { + if (!confirm('Are you sure you want to clear the cache?')) { + return; + } + fetch('/api/events/clear', { method: 'POST' }) + .then(response => response.json()) + .then(data => { + console.log(data.message); + fetchEvents(); + }) + .catch(error => console.error('Error clearing cache:', error)); + }); + + fetchEvents(); + setInterval(fetchEvents, 5000); // Refresh every 5 seconds +});