TC 5 өдөр өмнө
parent
commit
14a4a9b645

+ 12 - 0
dezukvmd/config/usbkvm.json

@@ -0,0 +1,12 @@
+{
+  "ListeningAddress": ":9000",
+  "USBKVMDevicePath": "/dev/ttyUSB0",
+  "AuxMCUDevicePath": "/dev/ttyACM0",
+  "VideoCaptureDevicePath": "/dev/video0",
+  "AudioCaptureDevicePath": "/dev/snd/pcmC1D0c",
+  "CaptureResolutionWidth": 1920,
+  "CaptureResolutionHeight": 1080,
+  "CaptureResolutionFPS": 25,
+  "USBKVMBaudrate": 115200,
+  "AuxMCUBaudrate": 115200
+}

BIN
dezukvmd/dezukvmd


+ 0 - 0
dezukvmd/www/img/cursor_overlay.psd → dezukvmd/resources/cursor_overlay.psd


BIN
dezukvmd/resources/favicon.png


BIN
dezukvmd/resources/logo.png


BIN
dezukvmd/www/favicon.png


+ 36 - 0
dezukvmd/www/img/font_logo.svg

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="1024px" height="256px" viewBox="0 0 1024 256" enable-background="new 0 0 1024 256" xml:space="preserve">
+<path fill="#2EBE7E" d="M231.785,196.207c0,23.753-19.256,43.01-43.01,43.01H54.676c-23.754,0-43.01-19.257-43.01-43.01V59.792
+	c0-23.754,19.256-43.01,43.01-43.01h134.098c23.754,0,43.01,19.255,43.01,43.01V196.207z"/>
+<path fill="#FFFFFF" d="M203.562,54.28v17.088c0,82.257-45.903,119.618-102.529,119.618l-32.102-0.002
+	c-1.386,7.795-2.074,16.296-2.074,25.634H49.769c0-11.644,0.988-22.218,2.954-31.886c-1.969-11.058-2.954-26.063-2.954-45.011
+	c0-47.188,38.253-85.441,85.441-85.441C152.297,54.28,169.385,62.825,203.562,54.28z M135.209,71.368
+	c-37.75,0-68.353,30.604-68.353,68.354c0,3.097,0.027,6.075,0.082,8.937c10.718-16.897,26.414-30.257,46.943-41.987l8.478,14.836
+	c-24.392,13.938-40.573,29.779-48.879,52.391h27.552c51.395,0,84.34-33.945,85.414-99.217c-11.722,1.14-22.615,0.414-36.051-1.602
+	C140.56,71.602,138.634,71.368,135.209,71.368z"/>
+<g>
+	<path fill="#2EBE7E" d="M275.729,71.932h34.453c36.546,0,60.695,17.709,60.695,59.408c0,41.697-24.149,60.533-59.085,60.533
+		h-36.063V71.932z M308.411,168.689c19.319,0,33.004-8.854,33.004-37.35c0-28.497-13.685-36.386-33.004-36.386h-3.864v73.735
+		H308.411z"/>
+	<path fill="#2EBE7E" d="M385.69,145.99c0-29.624,21.412-47.977,43.469-47.977c26.403,0,39.283,19.158,39.283,44.113
+		c0,5.151-0.645,10.143-1.289,12.396h-53.772c2.576,12.558,11.27,17.71,22.861,17.71c6.601,0,12.719-1.932,19.32-5.797l9.499,17.227
+		c-9.499,6.762-22.056,10.465-32.682,10.465C405.975,194.127,385.69,176.418,385.69,145.99z M444.131,135.686
+		c0-9.015-4.025-15.777-14.49-15.777c-7.889,0-14.812,4.991-16.744,15.777H444.131z"/>
+	<path fill="#2EBE7E" d="M480.68,176.579l37.189-53.934h-33.003v-22.378h68.746v15.294l-37.189,53.934h38.477v22.378H480.68V176.579
+		z"/>
+	<path fill="#2EBE7E" d="M567.943,156.938v-56.67H596.6v53.128c0,12.558,3.381,16.261,10.625,16.261
+		c6.441,0,10.465-2.576,15.295-9.338v-60.051h28.658v91.606h-23.346l-2.092-12.557h-0.645c-7.566,9.016-16.1,14.811-28.496,14.811
+		C576.475,194.127,567.943,179.799,567.943,156.938z"/>
+	<path fill="#2EBE7E" d="M676.455,71.932h28.818v47.494h0.482l33.971-47.494h31.555l-36.225,48.137l42.986,71.804h-31.395
+		l-28.336-49.104l-13.039,17.71v31.394h-28.818V71.932z"/>
+	<path fill="#2EBE7E" d="M771.766,71.932h30.43l13.684,54.738c3.543,12.879,5.957,25.115,9.338,38.317h0.805
+		c3.543-13.202,5.957-25.438,9.338-38.317l13.523-54.738h29.301l-35.9,119.941h-34.615L771.766,71.932z"/>
+	<path fill="#2EBE7E" d="M890.582,71.932h31.072l18.998,51.84c2.414,6.923,4.346,14.489,6.6,21.734h0.807
+		c2.414-7.245,4.346-14.812,6.6-21.734l18.354-51.84h31.234v119.941h-26.242V150.82c0-11.914,2.254-29.785,3.863-41.538h-0.805
+		l-9.982,28.657l-16.1,43.791h-15.777l-16.26-43.791l-9.66-28.657h-0.645c1.449,11.753,3.703,29.624,3.703,41.538v41.053h-25.76
+		V71.932z"/>
+</g>
+</svg>

BIN
dezukvmd/www/img/logo.png


+ 141 - 30
dezukvmd/www/index.html

@@ -1,31 +1,142 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta name="description" content="dezuKVM Management Interface">
-    <meta name="author" content="imuslab">
-    <meta name="csrf-token" content="">
-    <title>Connected | dezuKVM</title>
-    
-    <!-- OpenGraph Metadata -->
-    <meta property="og:title" content="dezuKVM Management Interface">
-    <meta property="og:description" content="A web-based management interface for dezuKVM">
-    <meta property="og:type" content="website">
-    <meta property="og:url" content="https://kvm.aroz.org">
-    <meta property="og:image" content="https://kvm.aroz.org/og.jpg">
-    <script src="js/jquery-3.7.1.min.js"></script>
-    <link rel="stylesheet" href="main.css">
-</head>
-<body>
-    <img id="remoteCapture" src="/stream" oncontextmenu="return false;"></img>
-    <div id="menu">
-        <button id="btnFullScreen" onclick="toggleFullScreen()">Fullscreen</button>
-        <button id="btnCtrlAltDel" onclick="sendCtrlAltDel()">Ctrl+Alt+Del</button>
-        <button id="btnFullScreen" onclick="switchMassStorageToKvm()">Switch Storage to KVM</button>
-        <button id="btnFullScreen" onclick="switchMassStorageToRemote()">Switch Storage to Remote</button>
-    </div>
-    <script src="ui.js"></script>
-    <script src="kvmevt.js"></script>
-</body>
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>DezuKVM | Dashboard</title>
+        <meta name="csrf_token" content="">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <link rel="icon" type="image/png" href="/favicon.png">
+        <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
+        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.css" integrity="sha512-ySrYzxj+EI1e9xj/kRYqeDL5l1wW0IWY8pzHNTIZ+vc1D3Z14UDNPbwup4yOUmlRemYjgUXsUZ/xvCQU2ThEAw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.js" integrity="sha512-Y/wIVu+S+XJsDL7I+nL50kAVFLMqSdvuLqF2vMoRqiMkmvcqFjEpEgeu6Rx8tpZXKp77J8OUpMKy0m3jLYhbbw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+        
+        <style>
+            body {
+                margin: 0;
+                height: 100vh;
+                font-family: 'Segoe UI', Arial, sans-serif;
+                background: #f9fafb;
+                color: #222;
+            }
+            .main-layout {
+                display: flex;
+                height: 100vh;
+            }
+            .sidebar {
+                width: 100px;
+                background: #f4f5f7; /* Changed from #222 to a whitish grey */
+                color: #222; /* Changed from #fff to dark text */
+                display: flex;
+                flex-direction: column;
+                justify-content: space-between;
+                border-right: 1px solid #e0e1e2;
+            }
+            .sidebar .menu-top {
+                padding: 32px 0 0 0;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+            }
+            .sidebar .logo {
+                width: 64px;
+                height: 64px;
+                margin-bottom: 32px;
+                border-radius: 12px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                overflow: hidden;
+            }
+            .sidebar .logo img {
+                width: 48px;
+                height: 48px;
+                object-fit: contain;
+            }
+            .sidebar .menu-options {
+                width: 100%;
+            }
+            .sidebar .menu-options .item {
+                padding: 16px 32px;
+                cursor: pointer;
+                transition: background 0.2s;
+                font-size: 1.1em;
+                color: #222;
+            }
+            .sidebar .menu-options .item:hover,
+            .sidebar .menu-options .item.active {
+                background: #e9ecef; /* Lighter hover/active background */
+            }
+            .sidebar .menu-bottom {
+                background: #f0f1f3; /* Lighter than sidebar */
+                padding: 24px 0 24px 0;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+            }
+            .sidebar .menu-bottom .item {
+                color: #555;
+                padding: 12px 32px;
+                width: 100%;
+                text-align: center;
+                cursor: pointer;
+                border-radius: 6px;
+                margin-bottom: 8px;
+                transition: background 0.2s, color 0.2s;
+            }
+            .sidebar .menu-bottom .item:hover {
+                background: #e9ecef;
+                color: #222;
+            }
+            .content {
+                flex: 1;
+                background: #f9fafb;
+                padding: 32px;
+                overflow-y: auto;
+            }
+            @media (prefers-color-scheme: dark) {
+                body, .content {
+                    background: #181a1b;
+                    color: #eee;
+                }
+                .sidebar {
+                    background: #181818;
+                    color: #fff;
+                }
+                .sidebar .menu-bottom {
+                    background: #222;
+                }
+                .sidebar .menu-options .item:hover,
+                .sidebar .menu-options .item.active {
+                    background: #222;
+                }
+            }
+        </style>
+    </head>
+    <body>
+        <div class="main-layout">
+            <nav class="sidebar">
+                <div>
+                    <div class="menu-top">
+                        <div class="logo">
+                            <img src="img/logo.png" alt="Logo">
+                        </div>
+                        <div class="menu-options">
+                            <div class="item active">Dashboard</div>
+                            <div class="item">Servers</div>
+                            <div class="item">Storage</div>
+                            <div class="item">Network</div>
+                            <div class="item">Users</div>
+                        </div>
+                    </div>
+                </div>
+                <div class="menu-bottom">
+                    <div class="item"><i class="cog icon"></i> Settings</div>
+                    <div class="item"><i class="sign-out icon"></i> Logout</div>
+                </div>
+            </nav>
+            <main class="content">
+                <!-- Main content goes here -->
+            </main>
+        </div>
+    </body>
 </html>

+ 12 - 1
dezukvmd/www/kvmevt.js

@@ -331,13 +331,24 @@ function startAudioWebSocket(quality="standard") {
         console.error("Audio WebSocket error", e);
     };
 
+    // Periodically resync scheduledTime to avoid drift
+    let lastResync = 0;
+    const RESYNC_INTERVAL = 60; // seconds
+
     function scheduleAudioPlayback() {
         if (!audioContext || audioQueue.length === 0) return;
 
-        // Use audioContext.currentTime to schedule buffers back-to-back
+        // Resync scheduledTime every RESYNC_INTERVAL seconds to avoid drift
         if (scheduledTime < audioContext.currentTime) {
             scheduledTime = audioContext.currentTime;
         }
+        if (scheduledTime - audioContext.currentTime > RESYNC_INTERVAL) {
+            scheduledTime = audioContext.currentTime;
+        }
+        if (audioContext.currentTime - lastResync > RESYNC_INTERVAL) {
+            scheduledTime = audioContext.currentTime;
+            lastResync = audioContext.currentTime;
+        }
 
         while (audioQueue.length > 0) {
             let floatBuf = audioQueue.shift();

+ 164 - 0
dezukvmd/www/login.html

@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>DezuKVM | Login</title>
+    <meta name="csrf_token" content="">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="icon" type="image/png" href="/favicon.png">
+    <style>
+        html, body {
+            height: 100%;
+            margin: 0;
+            padding: 0;
+            font-family: Arial, Helvetica, sans-serif;
+        }
+        body {
+            height: 100vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #f5f5f5;
+            color: #222;
+        }
+        .login-container {
+            background: #fff;
+            padding: 2rem 2.5rem;
+            border-radius: 8px;
+            box-shadow: 0 2px 16px rgba(0,0,0,0.08);
+            min-width: 420px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+
+        @media (prefers-color-scheme: dark) {
+            body {
+                background: #181a1b !important;
+                color: #f1f1f1 !important;
+            }
+            .login-container {
+                background: #23272a;
+                box-shadow: 0 2px 16px rgba(0,0,0,0.32);
+            }
+            .login-container .error-message {
+                background: #3a2323;
+                color: #ffb3b3;
+                border-color: #a94442;
+            }
+            input, .ui.input > input {
+                background: #181a1b !important;
+                color: #f1f1f1 !important;
+                border-color: #444 !important;
+            }
+
+            .ui.basic.button {
+                background: #23272a !important;
+                color: #f1f1f1 !important;
+                border-color: #444 !important;
+            }
+            .ui.basic.button:hover,
+            .ui.basic.button:focus {
+                background: #181a1b !important;
+                color: #fff !important;
+                border-color: #666 !important;
+            }
+        }
+        .login-container form {
+            width: 100%;
+            display: flex;
+            flex-direction: column;
+            gap: 1rem;
+        }
+        .login-container .error-message {
+            margin-top: 1rem;
+            color: #d32f2f;
+            background: #ffeaea;
+            border: 1px solid #f5c6cb;
+            border-radius: 4px;
+            padding: 0.5rem;
+            width: 100%;
+            text-align: center;
+            display: none;
+        }
+    </style>
+    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.css" integrity="sha512-ySrYzxj+EI1e9xj/kRYqeDL5l1wW0IWY8pzHNTIZ+vc1D3Z14UDNPbwup4yOUmlRemYjgUXsUZ/xvCQU2ThEAw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.js" integrity="sha512-Y/wIVu+S+XJsDL7I+nL50kAVFLMqSdvuLqF2vMoRqiMkmvcqFjEpEgeu6Rx8tpZXKp77J8OUpMKy0m3jLYhbbw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+</head>
+<body>
+    <div class="login-container ui basic segment">
+        <img src="img/font_logo.svg" alt="dezuKVM Logo" style="width: 150px; margin-bottom: 1rem;">
+        <p>Enter your password to access your IP-KVM</p>
+        <form id="loginForm" autocomplete="off">
+            <div class="ui action input" style="width:100%;">
+                <input type="password" id="password" name="password" placeholder="Password" required autofocus>
+                <button type="button" class="ui basic icon button" id="togglePassword" tabindex="-1">
+                    <i class="ui eye icon" id="eyeIcon"></i>
+                </button>
+            </div>
+            <button class="ui basic button" type="submit"><i class="ui green sign in alternate icon"></i> Login</button>
+        </form>
+        <div style="width:100%; margin-top: 1rem; display: flex; justify-content: space-between; font-size: 0.95em;">
+            <div>
+            <div class="ui breadcrumb">
+                <a href="/forgot-password" class="section">Forgot Password?</a>
+                <div class="divider"> / </div>
+                <a href="https://dezukvm.com" target="_blank" rel="noopener" class="section">DezuKVM</a>
+            </div>
+            </div>
+        </div>
+        <div class="error-message" id="errorMsg"></div>
+    </div>
+    <script>
+        // $cjax wrapper example (assumes similar to $.ajax)
+        function $cjax(options) {
+            var csrfToken = $('meta[name="csrf_token"]').attr('content');
+            if (!options.headers) options.headers = {};
+            options.headers['X-CSRF-Token'] = csrfToken;
+            return $.ajax(options);
+        }
+
+        $(function() {
+            $('#loginForm').on('submit', function(e) {
+                e.preventDefault();
+                $('#errorMsg').hide();
+                var password = $('#password').val();
+                $cjax({
+                    url: '/api/v1/login',
+                    method: 'POST',
+                    contentType: 'application/json',
+                    data: JSON.stringify({ password: password }),
+                    success: function(resp, status, xhr) {
+                        if (xhr.status === 200 && !(resp && resp.error)) {
+                            window.location.href = 'index.html';
+                        } else {
+                            var msg = resp && resp.error ? resp.error : 'Login failed.';
+                            $('#errorMsg').text(msg).show();
+                        }
+                    },
+                    error: function(xhr) {
+                        var msg = 'Login failed.';
+                        if (xhr.responseJSON && xhr.responseJSON.error) {
+                            msg = xhr.responseJSON.error;
+                        }
+                        $('#errorMsg').text(msg).show();
+                    }
+                });
+            });
+        });
+
+         $('#togglePassword').on('click', function() {
+            var input = $('#password');
+            var icon = $('#eyeIcon');
+            if (input.attr('type') === 'password') {
+                input.attr('type', 'text');
+                icon.removeClass('eye').addClass('eye slash');
+            } else {
+                input.attr('type', 'password');
+                icon.removeClass('eye slash').addClass('eye');
+            }
+        });
+    </script>
+</body>
+</html>

+ 31 - 0
dezukvmd/www/viewport.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="dezuKVM Management Interface">
+    <meta name="author" content="imuslab">
+    <meta name="csrf-token" content="">
+    <title>Connected | dezuKVM</title>
+    
+    <!-- OpenGraph Metadata -->
+    <meta property="og:title" content="dezuKVM Management Interface">
+    <meta property="og:description" content="A web-based management interface for dezuKVM">
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="https://kvm.aroz.org">
+    <meta property="og:image" content="https://kvm.aroz.org/og.jpg">
+    <script src="js/jquery-3.7.1.min.js"></script>
+    <link rel="stylesheet" href="main.css">
+</head>
+<body>
+    <img id="remoteCapture" src="/stream" oncontextmenu="return false;"></img>
+    <div id="menu">
+        <button id="btnFullScreen" onclick="toggleFullScreen()">Fullscreen</button>
+        <button id="btnCtrlAltDel" onclick="sendCtrlAltDel()">Ctrl+Alt+Del</button>
+        <button id="btnFullScreen" onclick="switchMassStorageToKvm()">Switch Storage to KVM</button>
+        <button id="btnFullScreen" onclick="switchMassStorageToRemote()">Switch Storage to Remote</button>
+    </div>
+    <script src="ui.js"></script>
+    <script src="kvmevt.js"></script>
+</body>
+</html>