From 06812231c167916c93bba1d10b40b3d9c800e152 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 28 Jan 2025 15:57:48 +0200 Subject: [PATCH 001/210] fixed missing python-bcrypt --- PKGBUILD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PKGBUILD b/PKGBUILD index c56da0f9..688fa133 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -53,6 +53,8 @@ depends=( python-aiofiles python-async-lru python-passlib + # python-bcrypt is needed for passlib + python-bcrypt python-pyotp python-qrcode python-periphery From 4039ae04835a466b3efa3172983be8bde06ce718 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 28 Jan 2025 16:00:27 +0200 Subject: [PATCH 002/210] =?UTF-8?q?Bump=20version:=204.49=20=E2=86=92=204.?= =?UTF-8?q?50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index dfb25c31..83d84254 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.49 +current_version = 4.50 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 688fa133..4ac0dda6 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.49 +pkgver=4.50 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 1dac1011..e81b511d 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.49" +__version__ = "4.50" diff --git a/setup.py b/setup.py index da1df8bd..f4dbf83f 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.49", + version="4.50", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 697ef549b9d78bcd02a63f9395731b89aa5accc0 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 30 Jan 2025 10:34:36 +0200 Subject: [PATCH 003/210] refactoring --- web/base.pug | 19 +- web/index.pug | 31 +- web/ipmi/index.pug | 17 +- web/kvm/index.html | 1018 +++++++++++++++++----------------- web/kvm/index.pug | 35 +- web/kvm/navbar-atx.pug | 19 +- web/kvm/navbar-gpio.pug | 7 +- web/kvm/navbar-health.pug | 24 +- web/kvm/navbar-macro.pug | 40 +- web/kvm/navbar-msd.pug | 114 ++-- web/kvm/navbar-shortcuts.pug | 89 +-- web/kvm/navbar-switch.pug | 29 +- web/kvm/navbar-system.pug | 244 ++++---- web/kvm/navbar-text.pug | 44 +- web/kvm/navbar.pug | 86 ++- web/kvm/window-about.pug | 38 +- web/kvm/window-keyboard.pug | 126 ++--- web/kvm/window-stream.pug | 79 +-- web/kvm/window-switch.pug | 95 ++-- web/kvm/window-webterm.pug | 13 +- web/login/index.html | 6 +- web/login/index.pug | 25 +- web/vnc/index.pug | 17 +- 23 files changed, 1150 insertions(+), 1065 deletions(-) diff --git a/web/base.pug b/web/base.pug index 4bb50099..004c849f 100644 --- a/web/base.pug +++ b/web/base.pug @@ -22,18 +22,21 @@ doctype html # # ============================================================================== -- var css_dir = "/share/css" -- var js_dir = "/share/js" -- var svg_dir = "/share/svg" -- var png_dir = "/share/png" +- + var css_dir = "/share/css" + var js_dir = "/share/js" + var svg_dir = "/share/svg" + var png_dir = "/share/png" + + title = "" + main_js = "" + body_class = "" + css_list = ["vars", "main"] -- var title = "" -- var main_js = "" -- var body_class = "" -- var css_list = ["vars", "main"] block vars + html(lang="en") head meta(charset="utf-8") diff --git a/web/index.pug b/web/index.pug index c84efb59..29de6da7 100644 --- a/web/index.pug +++ b/web/index.pug @@ -1,45 +1,48 @@ extends start.pug + append vars - - title = "PiKVM Index" - - main_js = "index/main" - - css_list = css_list.concat(["window", "modal", "index/index"]) + - + title = "PiKVM Index" + main_js = "index/main" + css_list = css_list.concat(["window", "modal", "index/index"]) + block start table tr - td(class="logo") + td.logo a(href="https://pikvm.org" target="_blank") - img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="PiKVM" height="40") + img.svg-gray(src=`${svg_dir}/logo.svg` alt="PiKVM" height="40") td table - tr #[td(colspan="2" class="title") The Open Source KVM over IP] + tr #[td.title(colspan="2") The Open Source KVM over IP] tr - td(colspan="2" class="copyright") + td.copyright(colspan="2") | Copyright © 2018-2024 #[a(target="_blank" href="mailto:mdevaev@gmail.com") Maxim Devaev] hr table td(class="server") td Server: - td #[a(id="kvmd-meta-server-host" target="_blank" href="/api/info")] + td #[a#kvmd-meta-server-host(target="_blank" href="/api/info")] hr - div(id="apps-box") + #apps-box h4 Loading ... - div(id="app-keyboard-warning") + #app-keyboard-warning hr - p(class="text") + p.text | Please note that when you are working with a KVM session or another application that captures the keyboard, | you can't use some keyboard shortcuts such as Ctrl+Alt+Del (which will be caught by your OS) or Ctrl+W (caught by your browser). - p(class="text") + p.text | To override this limitation you can use #[a(target="_blank" href="https://google.com/chrome") Google Chrome] | or #[a(target="_blank" href="https://chromium.org/Home") Chromium] in application mode. - div(id="app-text" class="code") + .code#app-text hr - p(class="text credits") + p.text.credits a(target="_blank" href="https://pikvm.org") PiKVM Project |   |   a(target="_blank" href="https://docs.pikvm.org") Documentation diff --git a/web/ipmi/index.pug b/web/ipmi/index.pug index e567fe38..c7729dfe 100644 --- a/web/ipmi/index.pug +++ b/web/ipmi/index.pug @@ -1,20 +1,23 @@ extends ../start.pug + append vars - - title = "PiKVM IPMI Info" - - main_js = "ipmi/main" - - index_link = true + - + title = "PiKVM IPMI Info" + main_js = "ipmi/main" + index_link = true + block start - p(class="text") + p.text | This PiKVM device has running #[b kvmd-ipmi] daemon and provides IPMI 2.0 interface for some basic | BMC operations like on/off/reset the server. - p(class="text") + p.text | #[b WARNING!] We strongly don't recommend you to use IPMI in untrusted networks because | this protocol is completely unsafe by design. In short, the authentication process for IPMI mandates | that the server send a salted SHA1 or MD5 hash of the requested user's password to the client, | prior to the client authenticating. - p(class="text") + p.text | #[b NEVER] use the same passwords for KVMD and IPMI users. And even better not to use IPMI. | Instead, you can directly use KVMD API via curl. Here some examples: - div(id="ipmi-text" class="code" style="max-height:200px") + .code#ipmi-text(style="max-height:200px") diff --git a/web/kvm/index.html b/web/kvm/index.html index da762585..d9de518a 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -151,7 +151,7 @@ - + @@ -196,74 +196,82 @@ Resolution: - + - JPEG quality: - - - - + JPEG quality: + + + + + - JPEG max fps: - - - - + JPEG max fps: + + + + + - H.264 kbps: - - - - + H.264 kbps: + + + + + - H.264 gop: - - - - + H.264 gop: + + + + + - Video mode: - -
- - - - - - -
- + Video mode: + + +
+ + + + + + +
+ - Orientation: - -
- - - - - - - - -
- + Orientation: + + +
+ + + + + + + + +
+ - Audio volume: - - - - + Audio volume: + + + + + - Microphone: + Microphone: +
@@ -275,7 +283,7 @@
- +

@@ -287,131 +295,143 @@ - Mouse mode: + Mouse mode:
-
- Keyboard & mouse (HID) settings -
- - - - + + + + + + + + + + + + + +
Swap Left Ctrl and Caps keys: -
- - -
+
+ Keyboard & mouse (HID) settings +
+ + + + + +
Swap Left Ctrl and Caps keys: + +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + - -
Mouse polling: + + +
Relative sensitivity: + + +
Squash relative moves: + +
+ + +
+
Reverse scrolling: + + + + + + + + +
Y: + +
+ + +
+
  X: + +
+ + +
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Mouse polling: - -
Relative sensitivity: - -
Squash relative moves: -
- - -
-
Reverse scrolling: - - - - - - - - -
Y: -
- - -
-
  X: -
- - -
-
-
Cumulative scrolling: -
- - -
-
Scroll rate: - -
Show the blue dot: -
- - -
-
-
-
-
- Web UI settings -
- - - - - - - - - -
Ask page close confirmation: -
- - -
-
Expand for the entire tab by default: -
- - -
-
-
-
+
Cumulative scrolling: + +
+ + +
+
Scroll rate: + + +
Show the blue dot: + +
+ + +
+
+
+
+
+ Web UI settings +
+ + + + + + + + + +
Ask page close confirmation: + +
+ + +
+
Expand for the entire tab by default: + +
+ + +
+
+
+
- + - + - + - + - + - + - +
Bad link mode (release keys immediately):Bad link mode (release keys immediately): +
@@ -420,7 +440,8 @@
Connect HID to Server:Connect HID to Server: +
@@ -429,7 +450,8 @@
Mouse jiggler:Mouse jiggler: +
@@ -438,7 +460,8 @@
Mute all input HID events:Mute all input HID events: +
@@ -447,7 +470,8 @@
Connect main USB to Server:Connect main USB to Server: +
@@ -456,7 +480,8 @@
Enable locator LED:Enable locator LED: +
@@ -468,7 +493,7 @@
- +
@@ -478,7 +503,8 @@
- +
Ask click confirmation:Ask click confirmation: +
@@ -489,10 +515,10 @@

- - + +
- +
@@ -519,7 +545,7 @@
Current image is broken!
Perhaps uploading was interrupted
Perhaps uploading was interrupted
@@ -587,29 +613,31 @@ Image: - + - + - + - - + + - +
Drive mode: -
- - - - -
-
Drive mode: + +
+ + + + +
+
 Writable:Writable: +
@@ -622,12 +650,12 @@

- - - + + +
+
-
- - - + + +
@@ -702,16 +730,16 @@
Record and play HID/ATX/GPIO actions
For security reasons, the record will not be saved on the PiKVM

- - - - + + + +

- + @@ -722,7 +750,8 @@
Script time:00:00:00.000:00:00.0
Scripted events:
- +
Infinite loop playback:Infinite loop playback: +
@@ -732,10 +761,10 @@

- +
- - + +
@@ -749,7 +778,7 @@
- + using host keymap @@ -759,7 +788,8 @@
- + - + - +
Slow typing:Slow typing: +
@@ -768,7 +798,8 @@
Hide input text:Hide input text: +
@@ -777,7 +808,8 @@
Ask paste confirmation:Ask paste confirmation: +
@@ -794,7 +826,7 @@ @@ -211,7 +211,7 @@ @@ -219,7 +219,7 @@ @@ -227,7 +227,7 @@ @@ -237,11 +237,11 @@ @@ -265,7 +265,7 @@ @@ -322,7 +322,7 @@ @@ -330,7 +330,7 @@ @@ -384,7 +384,7 @@ @@ -450,7 +450,7 @@ - -
- + for @@ -866,25 +898,26 @@
- - - - + + + +

- - - - - - + + + + + +

- + diff --git a/web/login/index.pug b/web/login/index.pug index aabb47ae..99097a27 100644 --- a/web/login/index.pug +++ b/web/login/index.pug @@ -1,33 +1,36 @@ extends ../base.pug + append vars - - title = "PiKVM Login" - - main_js = "login/main" - - css_list = css_list.concat(["window", "modal", "login/login"]) + - + title = "PiKVM Login" + main_js = "login/main" + css_list = css_list.concat(["window", "modal", "login/login"]) + block body form(action="javascript:void(0)") - div(id="login-box") - div(id="login") + #login-box + #login table tr td Username:  - td #[input(type="text" id="user-input" autocapitalize="off")] + td #[input#user-input(type="text" autocapitalize="off")] tr td Password:  - td #[input(type="password" id="passwd-input" autocapitalize="off")] + td #[input#passwd-input(type="password" autocapitalize="off")] tr td 2FA code:  - td #[input(type="text" id="code-input" placeholder="if enabled")] + td #[input#code-input(type="text" placeholder="if enabled")] tr td(colspan=2) hr tr td - td #[button(id="login-button" class="key" style="width:100%") Login] + td #[button.key#login-button(style="width:100%") Login] - ul(class="footer") - li(class="left") + ul.footer + li.left | This site is actively using JavaScript.#[br] | It doesn't contain ads, but is blocked by some ad filters.#[br] | Please turn it off to continue and reload the page. diff --git a/web/vnc/index.pug b/web/vnc/index.pug index 07bdb9a5..30d9a21e 100644 --- a/web/vnc/index.pug +++ b/web/vnc/index.pug @@ -1,20 +1,23 @@ extends ../start.pug + append vars - - title = "PiKVM VNC Info" - - main_js = "vnc/main" - - index_link = true + - + title = "PiKVM VNC Info" + main_js = "vnc/main" + index_link = true + block start - p(class="text") + p.text | This PiKVM device has running #[b kvmd-vnc] daemon and provides VNC access to the server. - p(class="text") + p.text | #[b WARNING!] We strongly don't recommend you to use VNC in untrusted networks without | enabled X.509 or TLS encryption. Otherwise your passwords are transmitted in a plain text | over the network. - p(class="text") + p.text | Your VNC client must support Tight JPEG compression and password authentication. | #[a(href="https://tigervnc.org") TigerVNC] is a good choice. | On Linux, this client will most likely be available for installation from the repository. | It can also be called vncviewer. - div(id="vnc-text" class="code" style="max-height:200px") + .code#vnc-text(style="max-height:200px") From d1a12f1f6afb590778a46bbc30c52d4887b478d4 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 30 Jan 2025 20:07:33 +0200 Subject: [PATCH 004/210] web: fixed slider height on firefox --- web/share/css/slider.css | 27 ++++++++++++--------------- web/share/css/x-mobile.css | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/web/share/css/slider.css b/web/share/css/slider.css index db743289..665339d6 100644 --- a/web/share/css/slider.css +++ b/web/share/css/slider.css @@ -20,26 +20,23 @@ *****************************************************************************/ -@supports (-webkit-appearance:none) { +input[type=range] { + cursor: pointer; + outline: none; + width: 100%; + box-shadow: none; + background: transparent; +} +@supports (-webkit-locale: auto) { /* WebKit only */ input[type=range] { - cursor: pointer; - outline: none; - width: 100%; - box-shadow: none; - background: transparent; margin: 8px 0 8px 0; -webkit-appearance: none; -webkit-tap-highlight-color: transparent; } } -@supports not (-webkit-appearance:none) { +@supports not (-webkit-locale: auto) { /* Firefox */ input[type=range] { - cursor: pointer; - outline: none; - width: 100%; - box-shadow: none; - margin-left: 0; - margin-right: 0; + margin: 1px 0 1px 0; } } input[type=range]:disabled { @@ -81,8 +78,8 @@ input[type=range]:disabled::-moz-range-track { input[type=range]::-moz-range-thumb { border: var(--border-intensive-2px); - height: 18px; - width: 18px; + height: 14px; + width: 14px; border-radius: 25px; background: var(--cs-thumb-default-bg); } diff --git a/web/share/css/x-mobile.css b/web/share/css/x-mobile.css index dfb8b1a7..46d0b5ea 100644 --- a/web/share/css/x-mobile.css +++ b/web/share/css/x-mobile.css @@ -91,7 +91,7 @@ ul#navbar li a.menu-button:hover:not(.active) { /* ===== slider.css ===== */ /*@media only screen and (orientation: portrait) { - @supports (-webkit-appearance: none) { + @supports (-webkit-locale: auto) { input[type=range] { margin: 20px 0 20px 0 !important; } From 3b5e5390129af92fd72223b937489e118287adce Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 30 Jan 2025 20:08:30 +0200 Subject: [PATCH 005/210] web fixes, verbose video modes name --- web/kvm/index.html | 26 +++++++++++++------------- web/kvm/navbar-system.pug | 10 +++++----- web/kvm/navbar.pug | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/web/kvm/index.html b/web/kvm/index.html index d9de518a..c45ed827 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -175,7 +175,7 @@
Ask the magic confirmation:Ask the magic confirmation: +
@@ -900,7 +933,7 @@
  • Switch @@ -929,7 +963,8 @@
  • - + - -
    Ask ATX click confirmation:Ask ATX click confirmation: +
    @@ -971,21 +1006,21 @@

    Hold
    -
    +
    Mid

    Hold
    -
    +

    Hold
    Right
    -
    +
    Up
    @@ -1008,74 +1043,74 @@
    Esc
    -
    +
     
    -
    +
    F1
    -
    +
    F2
    -
    +
    F3
    -
    +
    F4
    -
    +
     
    -
    +
    F5
    -
    +
    F6
    -
    +
    F7
    -
    +
    F8
    -
    +
     
    -
    +
    F9
    -
    +
    F10
    -
    +
    F11
    -
    +
    F12
    @@ -1087,67 +1122,67 @@
    ~
    `
    -
    +
    !
    1
    -
    +
    @
    2
    -
    +
    #
    3
    -
    +
    $
    4
    -
    +
    %
    5
    -
    +
    ^
    6
    -
    +
    &
    7
    -
    +
    *
    8
    -
    +
    (
    9
    -
    +
    )
    0
    -
    +
    _
    -
    -
    +
    +
    =
    -
    +
    @@ -1158,67 +1193,67 @@

    -
    +
    Q
    -
    +
    W
    -
    +
    E
    -
    +
    R
    -
    +
    T
    -
    +
    Y
    -
    +
    U
    -
    +
    I
    -
    +
    O
    -
    +
    P
    -
    +
    {
    [
    -
    +
    }
    ]
    -
    +
    |
    \
    @@ -1229,62 +1264,62 @@

    Caps Lock
    -
    +
    A
    -
    +
    S
    -
    +
    D
    -
    +
    F
    -
    +
    G
    -
    +
    H
    -
    +
    J
    -
    +
    K
    -
    +
    L
    -
    +
    :
    ;
    -
    +
    "
    '
    -
    +
    Enter
    @@ -1295,57 +1330,57 @@

    Shift
    -
    +
    Z
    -
    +
    X
    -
    +
    C
    -
    +
    V
    -
    +
    B
    -
    +
    N
    -
    +
    M
    -
    +
    <
    ,
    -
    +
    >
    .
    -
    +
    ?
    /
    -
    +

    Shift
    @@ -1356,37 +1391,37 @@

    Ctrl
    -
    +

    Win
    -
    +

    Alt
    -
    +
    -
    +

    Alt
    -
    +

    Win
    -
    +

    Menu
    -
    +

    Ctrl
    @@ -1399,12 +1434,12 @@

    Pt/Sq
    -
    +

    ScrLk
    -
    +
    P/Brk
    @@ -1416,12 +1451,12 @@
    Ins
    -
    +
    Home
    -
    +
    PgUp
    @@ -1432,12 +1467,12 @@
    Del
    -
    +
    End
    -
    +
    PgDn
    @@ -1448,12 +1483,12 @@
     
    -
    +
    -
    +
     
    @@ -1463,12 +1498,12 @@
    -
    +
    -
    +
    @@ -1480,15 +1515,15 @@
     
    -
    +
     
    -
    +
     
    -
    +
    PWR
    @@ -1500,17 +1535,17 @@

    NmLk
    -
    +
    /
    -
    +
    *
    -
    +
    -
    @@ -1521,17 +1556,17 @@
    7
    Home
    -
    +
    8
    -
    +
    9
    PgUp
    -
    +
     
    @@ -1541,17 +1576,17 @@
    4
    -
    +
    5

    -
    +
    6
    -
    +
    +
    @@ -1562,17 +1597,17 @@
    1
    End
    -
    +
    2
    -
    +
    3
    PgDn
    -
    +
     
    @@ -1582,16 +1617,16 @@
    0
    Ins
    -
    +
     
    -
    +
    .
    Del
    -
    +
    Ent
    @@ -1645,7 +1680,7 @@
    Esc
    -
    +
    F1
    @@ -1662,7 +1697,7 @@
    F4
    -
    +
    F5
    @@ -1679,7 +1714,7 @@
    F8
    -
    +
    F9
    @@ -1696,37 +1731,37 @@
    F12
    -
    +

    Pt/Sq
    -
    +

    ScrLk
    -
    +
    P/Brk
    -
    +
    Ins
    -
    +
    Home
    -
    +
    End
    -
    +
    Del
    @@ -1737,68 +1772,68 @@
    ~
    `
    -
    +
    !
    1
    -
    +
    @
    2
    -
    +
    #
    3
    -
    +
    $
    4
    -
    +
    %
    5
    -
    +
    ^
    6
    -
    +
    &
    7
    -
    +
    *
    8
    -
    +
    (
    9
    -
    +
    )
    0
    -
    +
    _
    -
    -
    +
    +
    =
    -
    -
    +
    +
    @@ -1808,68 +1843,68 @@

    -
    +
    Q
    -
    +
    W
    -
    +
    E
    -
    +
    R
    -
    +
    T
    -
    +
    Y
    -
    +
    U
    -
    +
    I
    -
    +
    O
    -
    +
    P
    -
    +
    {
    [
    -
    +
    }
    ]
    -
    -
    +
    +
    |
    \
    @@ -1879,63 +1914,63 @@

    Caps Lock
    -
    +
    A
    -
    +
    S
    -
    +
    D
    -
    +
    F
    -
    +
    G
    -
    +
    H
    -
    +
    J
    -
    +
    K
    -
    +
    L
    -
    +
    :
    ;
    -
    +
    `
    '
    -
    -
    +
    +
    Enter
    @@ -1945,67 +1980,67 @@

    Shift
    -
    +
    Z
    -
    +
    X
    -
    +
    C
    -
    +
    V
    -
    +
    B
    -
    +
    N
    -
    +
    M
    -
    +
    <
    ,
    -
    +
    >
    .
    -
    +
    ?
    /
    -
    +
    PgUp
    -
    +
    -
    +
    PgDn
    @@ -2016,57 +2051,57 @@

    Ctrl
    -
    +

    Win
    -
    +

    Alt
    -
    -
    +
    +
    -
    +

    Alt
    -
    +

    Win
    -
    +

    Menu
    -
    +

    Shift
    -
    +

    Ctrl
    -
    +
    -
    +
    -
    +
    @@ -2075,7 +2110,7 @@
    -
    +
    Switch settings
    @@ -2089,7 +2124,7 @@
    + @@ -2118,7 +2153,7 @@
    Manufacturer:
    Data: - +
    @@ -2128,8 +2163,8 @@
    - + +
    @@ -2138,62 +2173,45 @@
    - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + +
    Selected port: - - - -     - -
    Inactive port: - - - -     - -
    Blinking beacon: - - - -     - -
    Selected port: + + + +     + +
    Inactive port: + + + +     + +
    Blinking beacon: + + + +     + +

    @@ -2209,32 +2227,32 @@
    Flashing downlink: - - - -     - -
    Bootloader mode: - - - -     - -
    Flashing downlink: + + + +     + +
    Bootloader mode: + + + +     + +
    diff --git a/web/kvm/index.pug b/web/kvm/index.pug index ef694dde..6bf69430 100644 --- a/web/kvm/index.pug +++ b/web/kvm/index.pug @@ -1,30 +1,33 @@ extends ../base.pug + append vars - - title = "PiKVM Session" - - main_js = "kvm/main" - - body_class = "body-no-select" - - css_list = css_list.concat(["navbar", "window", "modal", "led", "slider", "switch", "radio", "progress", "keypad", "tabs"]) - - css_list = css_list.concat(["kvm/stream", "kvm/hid", "kvm/msd", "kvm/system", "kvm/keyboard", "kvm/about"]) + - + title = "PiKVM Session" + main_js = "kvm/main" + body_class = "body-no-select" + css_list = css_list.concat(["navbar", "window", "modal", "led", "slider", "switch", "radio", "progress", "keypad", "tabs"]) + css_list = css_list.concat(["kvm/stream", "kvm/hid", "kvm/msd", "kvm/system", "kvm/keyboard", "kvm/about"]) + block body include navbar.pug include windows.pug - ul(class="navbar-bg-tips") - li(class="left") - pre(id="kvmd-meta-tips-left") - li(class="right") - pre(id="kvmd-meta-tips-right") + ul.navbar-bg-tips + li.left + pre#kvmd-meta-tips-left + li.right + pre#kvmd-meta-tips-right - ul(class="footer") - li(class="left") - span(id="kvmd-meta-server-host" title="Server name (see System/About)") + ul.footer + li.left + span#kvmd-meta-server-host(title="Server name (see System/About)") |   |   - span(id="kvmd-version-kvmd" title="KVMD version") + span#kvmd-version-kvmd(title="KVMD version") |   |   - span(id="kvmd-version-streamer" title="Streamer version") - li(class="right") + span#kvmd-version-streamer(title="Streamer version") + li.right a(target="_blank" href="https://pikvm.org") PiKVM Project |   |   a(target="_blank" href="https://docs.pikvm.org") Documentation diff --git a/web/kvm/navbar-atx.pug b/web/kvm/navbar-atx.pug index 6800bb9c..a44708b3 100644 --- a/web/kvm/navbar-atx.pug +++ b/web/kvm/navbar-atx.pug @@ -1,17 +1,18 @@ -li(id="atx-dropdown" class="right feature-disabled") - a(class="menu-button" href="#") +li.right.feature-disabled#atx-dropdown + a.menu-button(href="#") +navbar_led("atx-power-led", "led-atx-power") +navbar_led("atx-hdd-led", "led-atx-hdd") span ATX - div(class="menu") - div(class="text") + + .menu + .text b Control the server's power#[br] sub Use the short click for ACPI shutdown hr - +menu_switch("atx-ask-switch", "Ask click confirmation", true, true) + +menu_switch_table("atx-ask-switch", true, true) Ask click confirmation: hr - div(class="buttons") - button(disabled data-force-hide-menu id="atx-power-button") • Click Power #[sup #[i short]] - button(disabled data-force-hide-menu id="atx-power-button-long") • Click Power #[sup #[i long]] + .buttons + button#atx-power-button(disabled data-force-hide-menu) • Click Power #[sup #[i short]] + button#atx-power-button-long(disabled data-force-hide-menu) • Click Power #[sup #[i long]] hr - button(disabled data-force-hide-menu id="atx-reset-button") • Click Reset + button#atx-reset-button(disabled data-force-hide-menu) • Click Reset diff --git a/web/kvm/navbar-gpio.pug b/web/kvm/navbar-gpio.pug index 1693212f..59884d22 100644 --- a/web/kvm/navbar-gpio.pug +++ b/web/kvm/navbar-gpio.pug @@ -1,4 +1,5 @@ -li(id="gpio-dropdown" class="right feature-disabled") - a(class="menu-button" id="gpio-menu-button" href="#") +li.right.feature-disabled#gpio-dropdown + a.menu-button#gpio-menu-button(href="#") span GPIO - div(id="gpio-menu" class="menu") + + .menu#gpio-menu diff --git a/web/kvm/navbar-health.pug b/web/kvm/navbar-health.pug index baaee79f..f33e3482 100644 --- a/web/kvm/navbar-health.pug +++ b/web/kvm/navbar-health.pug @@ -1,32 +1,34 @@ -div(id="hw-health-dropdown" class="hidden") - li(class="left") - a(class="menu-button" href="#") +.hidden#hw-health-dropdown + li.left + a.menu-button(href="#") +navbar_led("hw-health-undervoltage-led", "led-undervoltage", "hidden") +navbar_led("hw-health-overheating-led", "led-overheating", "hidden") - div(class="menu") + + .menu +menu_message("warning", "Raspberry Pi's health is at risk") | This is not a drill! A red icon indicates a current issue,#[br] | a yellow one that was observed since the device booted up - div(id="hw-health-message-undervoltage" class="hidden") + .hidden#hw-health-message-undervoltage hr +menu_message("led-undervoltage", "Undervoltage detected", "led-gray") | Make sure your power supply and cabling are providing#[br] | enough power to the Raspberry Pi (3A minimum) - div(id="hw-health-message-overheating" class="hidden") + .hidden#hw-health-message-overheating hr +menu_message("led-overheating", "Overheating detected", "led-gray") | Frequency capping due to overheating,#[br] | please improve cooling of the Raspberry Pi -div(id="fan-health-dropdown" class="hidden") - li(class="left") - a(class="menu-button" href="#") +.hidden#fan-health-dropdown + li.left + a.menu-button(href="#") +navbar_led("fan-health-led", "led-fan", "hidden") - div(class="menu") + + .menu +menu_message("warning", "Raspberry Pi's health is at risk") | This is not a drill! A red icon indicates a current issue,#[br] | a yellow one that was observed in the past - div(id="fan-health-message-fail") + #fan-health-message-fail hr +menu_message("led-fan", "Fan failed", "led-gray") | A fan error occured, please #[a(href="/api/log?seek=3600&follow=1" target="_blank") check the log] diff --git a/web/kvm/navbar-macro.pug b/web/kvm/navbar-macro.pug index cebc6678..d4240a5f 100644 --- a/web/kvm/navbar-macro.pug +++ b/web/kvm/navbar-macro.pug @@ -1,30 +1,36 @@ -li(id="macro-dropdown" class="right") - a(class="menu-button" href="#") +li.right#macro-dropdown + a.menu-button(href="#") +navbar_led("hid-recorder-led", "led-gear") span Macro - div(class="menu") - div(class="text") + + .menu + .text b Record and play HID/ATX/GPIO actions#[br] sub For security reasons, the record will not be saved on the PiKVM hr - div(class="buttons buttons-row") - button(disabled data-force-hide-menu id="hid-recorder-record" class="row25") • Rec - button(disabled id="hid-recorder-stop" class="row25") Stop - button(disabled id="hid-recorder-play" class="row25") Play - button(disabled id="hid-recorder-clear" class="row25") Clear + + .buttons.buttons-row + button.row25#hid-recorder-record(disabled data-force-hide-menu) • Rec + button.row25#hid-recorder-stop(disabled) Stop + button.row25#hid-recorder-play(disabled) Play + button.row25#hid-recorder-clear(disabled) Clear hr - table(class="kv") + + table.kv tr td Script time: - td(colspan="2" id="hid-recorder-time" class="value") 00:00:00.0 + td.value#hid-recorder-time(colspan="2") 00:00:00.0 tr td Scripted events: - td(id="hid-recorder-events-count" class="value") 0 + td.value#hid-recorder-events-count 0 td #[sup #[i include delays]] hr - +menu_switch("hid-recorder-loop-switch", "Infinite loop playback", false, false) + + +menu_switch_table("hid-recorder-loop-switch", false, false) Infinite loop playback: hr - input(type="file" id="hid-recorder-new-script-file") - div(class="buttons buttons-row") - button(disabled id="hid-recorder-upload" class="row50") Upload script - button(disabled id="hid-recorder-download" class="row50") Download script + + input#hid-recorder-new-script-file(type="file") + + .buttons.buttons-row + button.row50#hid-recorder-upload(disabled) Upload script + button.row50#hid-recorder-download(disabled) Download script diff --git a/web/kvm/navbar-msd.pug b/web/kvm/navbar-msd.pug index 6ca4323e..6d7ba08d 100644 --- a/web/kvm/navbar-msd.pug +++ b/web/kvm/navbar-msd.pug @@ -1,100 +1,108 @@ -li(id="msd-dropdown" class="right feature-disabled") - a(class="menu-button" href="#") +li.right.feature-disabled#msd-dropdown + a.menu-button(href="#") +navbar_led("msd-led", "led-msd") span Drive - div(id="msd-menu" class="menu") - div(class="text") + + .menu#msd-menu + .text b Mass Storage Drive: - span(id="msd-status") + span#msd-status br hr - div(id="msd-message-offline" class="hidden") + + .hidden#msd-message-offline +menu_message("warning", "Mass Storage Drive is offline") hr - div(id="msd-message-image-broken" class="hidden") + .hidden#msd-message-image-broken +menu_message("warning", "Current image is broken!") - | Perhaps uploading was interrupted#[br] + | Perhaps uploading was interrupted hr - div(id="msd-message-too-big-for-dvd" class="hidden") + .hidden#msd-message-too-big-for-dvd +menu_message("warning", "Current image is too big for DVD!") | The maximum is 31.6GiB. Please switch to the Flash mode. hr - div(id="msd-message-out-of-storage" class="hidden") + .hidden#msd-message-out-of-storage +menu_message("warning", "Current image is out of storage") | This image was connected manually using #[b kvmd-otgmsd] hr - div(id="msd-message-rw-enabled" class="hidden") + .hidden#msd-message-rw-enabled +menu_message("warning", "Read-write mode is enabled") | Do not turn off PiKVM while this is active to prevent#[br] | filesystem corruption. Use read-only mode where possible,#[br] | as writing to SD card often can reduce its lifespan. hr - div(id="msd-message-downloads" class="hidden") + .hidden#msd-message-downloads +menu_message("info", "The image is being downloaded from PiKVM") | Please wait hr - table(class="kv") + + table.kv tr td Image: - td(width="100%") #[select(disabled id="msd-image-selector")] - td #[button(disabled id="msd-download-button" title="Download image") #[b   ⇩  ]] - td #[button(disabled id="msd-remove-button" title="Remove image") #[b   ×  ]] - table(class="kv") + td(width="100%") #[select#msd-image-selector(disabled)] + td #[button#msd-download-button(disabled title="Download image") #[b   ⇩  ]] + td #[button#msd-remove-button(disabled title="Remove image") #[b   ×  ]] + + table.kv tr - td Drive #[a(target="_blank" href="https://docs.pikvm.org/msd") mode]: - td - div(class="radio-box") - input(checked type="radio" id="msd-mode-radio-cdrom" name="msd-mode-radio" value="1") - label(for="msd-mode-radio-cdrom") CD/DVD - input(type="radio" id="msd-mode-radio-flash" name="msd-mode-radio" value="0") - label(for="msd-mode-radio-flash") Flash + +menu_radio_td2("msd-mode-radio", [ + {title: "CD/DVD", value: "1", checked: true}, + {title: "Flash", value: "0"}, + ]) Drive #[a(target="_blank" href="https://docs.pikvm.org/msd") mode]: td   - +menu_switch_notable("msd-rw-switch", "Writable", false, false) + +menu_switch_td2("msd-rw-switch", false, false) Writable: hr - div(id="msd-storages") + + #msd-storages hr - div(class="buttons buttons-row") - button(disabled id="msd-select-new-button" class="row50") Select image to upload - button(disabled id="msd-upload-new-button" class="row25") Upload - button(disabled id="msd-abort-new-button" class="row25") Abort - div(id="msd-message-another-user-uploads" class="hidden") - hr + + .buttons.buttons-row + button.row50#msd-select-new-button(disabled) Select image to upload + button.row25#msd-upload-new-button(disabled) Upload + button.row25#msd-abort-new-button(disabled) Abort + hr + + .hidden#msd-message-another-user-uploads +menu_message("info", "Another user uploads an image") - div(id="msd-new-sub" class="hidden") hr - table(class="kv") + + .hidden#msd-new-sub + table.kv tr td Specify a local file: - td #[input(type="file" id="msd-new-file")] + td #[input#msd-new-file(type="file")] tr td #[b Or] paste a URL: - td #[input(type="text" id="msd-new-url" style="width: 100%")] - tr(id="msd-new-part" class="hidden") + td #[input#msd-new-url(type="text" style="width: 100%")] + tr.hidden#msd-new-part td Upload partition: - td(width="100%") #[select(id="msd-new-part-selector")] - div(id="msd-uploading-sub" class="hidden") + td(width="100%") #[select#msd-new-part-selector] hr - table(class="kv") + + .hidden#msd-uploading-sub + table.kv tr td New image: - td(id="msd-uploading-name" class="value") + td.value#msd-uploading-name tr td Upload size: - td(id="msd-uploading-size" class="value") - div(class="text") - div(id="msd-uploading-progress" class="progress") - span(id="msd-uploading-progress-value" class="progress-value") - div(id="msd-new-tips" class="hidden") + td.value#msd-uploading-size + .text + .progress#msd-uploading-progress + span.progress-value#msd-uploading-progress-value hr - table(class="kv") + + .hidden#msd-new-tips + table.kv tr - td(class="value") Note: + td.value Note: td • Don't close the browser page until the upload is complete. tr td td • To speed up the upload, close the stream window. - hr - div(class="buttons buttons-row") - button(disabled id="msd-connect-button" class="row50") Connect drive to Server - button(disabled id="msd-disconnect-button" class="row25") Disconnect - button(disabled id="msd-reset-button" class="row25") Reset + hr + + .buttons.buttons-row + button.row50#msd-connect-button Connect drive to Server + button.row25#msd-disconnect-button Disconnect + button.row25#msd-reset-button Reset diff --git a/web/kvm/navbar-shortcuts.pug b/web/kvm/navbar-shortcuts.pug index d020b415..2f1b41c7 100644 --- a/web/kvm/navbar-shortcuts.pug +++ b/web/kvm/navbar-shortcuts.pug @@ -1,47 +1,51 @@ -li(id="shortcuts-dropdown" class="right") - a(class="menu-button" href="#") Shortcuts - div(id="shortcuts-menu" class="menu") - div(class="text") +li.right#shortcuts-dropdown + a.menu-button(href="#") Shortcuts + + .menu#shortcuts-menu + .text b Quick keyboard shortcuts#[br] sub Also see #[i System → Show keyboard] hr - div(class="buttons") - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="CapsLock" class="row50") + + .buttons + .buttons-row + button.row50(data-force-hide-menu data-shortcut="CapsLock") | • Caps Lock   - img(class="inline-lamp-small hid-keyboard-caps-led led-gray" src=`${svg_dir}/led-square.svg`) - button(data-force-hide-menu data-shortcut="MetaLeft" class="row50") • Left Win + img.inline-lamp-small.hid-keyboard-caps-led.led-gray(src=`${svg_dir}/led-square.svg`) + button.row50(data-force-hide-menu data-shortcut="MetaLeft") • Left Win hr - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="AltLeft ShiftLeft" class="row50") • Alt+Shift - button(data-force-hide-menu data-shortcut="ControlLeft KeyW" class="row50") • Ctrl+W - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="ControlLeft ShiftLeft" class="row50") • Ctrl+Shift - button(data-force-hide-menu data-shortcut="AltLeft Tab" class="row50") • Alt+Tab - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="ShiftLeft ShiftRight" class="row50") • Shift+Shift - button(data-force-hide-menu data-shortcut="AltLeft Enter" class="row50") • Alt+Enter - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="MetaLeft Space" class="row50") • Win+Space - button(data-force-hide-menu data-shortcut="AltLeft F4" class="row50") • Alt+F4 + .buttons-row + button.row50(data-force-hide-menu data-shortcut="AltLeft ShiftLeft") • Alt+Shift + button.row50(data-force-hide-menu data-shortcut="ControlLeft KeyW") • Ctrl+W + .buttons-row + button.row50(data-force-hide-menu data-shortcut="ControlLeft ShiftLeft") • Ctrl+Shift + button.row50(data-force-hide-menu data-shortcut="AltLeft Tab") • Alt+Tab + .buttons-row + button.row50(data-force-hide-menu data-shortcut="ShiftLeft ShiftRight") • Shift+Shift + button.row50(data-force-hide-menu data-shortcut="AltLeft Enter") • Alt+Enter + .buttons-row + button.row50(data-force-hide-menu data-shortcut="MetaLeft Space") • Win+Space + button.row50(data-force-hide-menu data-shortcut="AltLeft F4") • Alt+F4 hr - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="ControlLeft AltLeft F1" class="row50") • Ctrl+Alt+F1 - button(data-force-hide-menu data-shortcut="MetaLeft KeyL" class="row50") • Win+L - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="ControlLeft AltLeft F2" class="row50") • Ctrl+Alt+F2 - button(data-force-hide-menu data-shortcut="PrintScreen" class="row50") • Print Screen + .buttons-row + button.row50(data-force-hide-menu data-shortcut="ControlLeft AltLeft F1") • Ctrl+Alt+F1 + button.row50(data-force-hide-menu data-shortcut="MetaLeft KeyL") • Win+L + .buttons-row + button.row50(data-force-hide-menu data-shortcut="ControlLeft AltLeft F2") • Ctrl+Alt+F2 + button.row50(data-force-hide-menu data-shortcut="PrintScreen") • Print Screen hr - div(class="buttons-row") - button(data-force-hide-menu data-shortcut="ControlLeft AltLeft Delete" class="row50") • Ctrl+Alt+Del - button(data-force-hide-menu data-shortcut="Power" class="row50") • Power + .buttons-row + button.row50(data-force-hide-menu data-shortcut="ControlLeft AltLeft Delete") • Ctrl+Alt+Del + button.row50(data-force-hide-menu data-shortcut="Power") • Power hr - div(class="text") + + .text | ↓ • Alt+SysRq+... linux magic | #[a(target="_blank" href="https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html") help] hr - div(class="buttons") - div(class="buttons-row") + + .buttons + .buttons-row - let sysrq = { "F": "Call the OOM killer to kill a memory hog process", @@ -50,9 +54,14 @@ li(id="shortcuts-dropdown" class="right") "T": "Dump a list of current tasks and their information to the console", } each title, key in sysrq - button(data-shortcut=`AltLeft PrintScreen Key${key}` data-shortcut-confirm="hid-sysrq-ask-switch" class="row25" style="text-align: center;" title=`${title}`) #{key} + button.row25( + data-shortcut=`AltLeft PrintScreen Key${key}` + data-shortcut-confirm="hid-sysrq-ask-switch" + style="text-align: center" + title=`${title}` + ) #{key} hr - div(class="buttons-row") + .buttons-row - sysrq = { "R": "Turn off keyboard raw mode, set it to XLATE", @@ -63,6 +72,12 @@ li(id="shortcuts-dropdown" class="right") "B": "Immediately reboot the system without syncing or unmounting disks", } each title, key in sysrq - button(data-shortcut=`AltLeft PrintScreen Key${key}` data-shortcut-confirm="hid-sysrq-ask-switch" class="row16" style="text-align: center;" title=`${title}`) #{key} + button.row16( + data-shortcut=`AltLeft PrintScreen Key${key}` + data-shortcut-confirm="hid-sysrq-ask-switch" + style="text-align: center" + title=`${title}` + ) #{key} hr - +menu_switch("hid-sysrq-ask-switch", "Ask the magic confirmation", true, true) + + +menu_switch_table("hid-sysrq-ask-switch", true, true) Ask the magic confirmation: diff --git a/web/kvm/navbar-switch.pug b/web/kvm/navbar-switch.pug index ef8bfab4..ded526c1 100644 --- a/web/kvm/navbar-switch.pug +++ b/web/kvm/navbar-switch.pug @@ -1,23 +1,28 @@ -li(id="switch-dropdown" class="right feature-disabled") - a(class="menu-button" id="switch-menu-button" href="#") +li.right.feature-disabled#switch-dropdown + a.menu-button#switch-menu-button(href="#") +navbar_led("switch-atx-power-led", "led-atx-power") +navbar_led("switch-atx-hdd-led", "led-atx-hdd") - span Switch #[i #[sub(id="switch-active-port") ]] - div(id="switch-menu" class="menu") - table(style="border-spacing: 0px;") + span Switch #[i #[sub#switch-active-port]] + + .menu#switch-menu + table(style="border-spacing: 0px") tr td - div(class="text") + .text b #[a(target="_blank" href="https://docs.pikvm.org/switch") PiKVM Switch] is attached#[br] sub Select a port or perform any available action like ATX click td - div(class="text") - button(data-force-hide-menu data-show-window="switch-window" class="small") • Settings + .text + button.small(data-force-hide-menu data-show-window="switch-window") • Settings hr - div(id="switch-message-update" class="hidden") + + .hidden#switch-message-update +menu_message("info", "Good news! Your switch is ready to get the firmware update") - | Please #[a(target="_blank" href="https://docs.pikvm.org/switch/#firmware-updating") follow the instructions] when you decide to install it. + | Please #[a(target="_blank" href="https://docs.pikvm.org/switch/#firmware-updating") follow the instructions] + | when you decide to install it. hr - +menu_switch("switch-atx-ask-switch", "Ask ATX click confirmation", true, true) + + +menu_switch_table("switch-atx-ask-switch", true, true) Ask ATX click confirmation: hr - table(id="switch-chain" class="kv") + + table.kv#switch-chain diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index 5c675601..36256edb 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -1,148 +1,138 @@ -li(id="system-dropdown" class="right") - a(class="menu-button" href="#") +li.right#system-dropdown + a.menu-button(href="#") +navbar_led("link-led", "led-link") +navbar_led("stream-led", "led-video") +navbar_led("hid-keyboard-led", "led-hid-keyboard") +navbar_led("hid-mouse-led", "led-hid-mouse") span System - div(id="system-menu" class="menu") - table(class="kv") + + .menu#system-menu + table.kv tr - td(class="value") Runtime settings & tools - td(id="system-tool-webterm" class="feature-disabled") #[button(data-force-hide-menu data-show-window="webterm-window" class="small") • Term] - td(id="system-tool-about") #[button(data-force-hide-menu data-show-window="about-window" class="small") • About] - td(id="system-tool-log") #[button(data-force-hide-menu id="open-log-button" class="small") • Log] - td(id="system-tool-wol" class="feature-disabled") - button(data-force-hide-menu class="__gpio-button-__wol__ __gpio-button small" data-channel="__wol__" - data-confirm="Are you sure to send Wake-on-LAN packet to the server?") • WoL + td.value + | Runtime settings & tools + td.feature-disabled#system-tool-webterm + button.small(data-force-hide-menu data-show-window="webterm-window") • Term + td#system-tool-about + button.small(data-force-hide-menu data-show-window="about-window") • About + td#system-tool-log + button.small#open-log-button(data-force-hide-menu) • Log + td.feature-disabled#system-tool-wol + button( + data-force-hide-menu + class="__gpio-button-__wol__ __gpio-button small" + data-channel="__wol__" + data-confirm="Are you sure to send Wake-on-LAN packet to the server?" + ) • WoL hr - div(id="stream-message-no-webrtc" class="hidden") + + .hidden#stream-message-no-webrtc +menu_message("warning", "WebRTC is not supported by this browser") hr - div(id="stream-message-no-vd" class="hidden") + .hidden#stream-message-no-vd +menu_message("warning", "Direct HTTP H.264 streaming is not supported") hr - div(id="stream-message-no-h264" class="hidden") + .hidden#stream-message-no-h264 +menu_message("warning", "H.264 is not supported by this browser") hr - table(class="kv") - tr(id="stream-resolution" class="feature-disabled") + + table.kv + tr.feature-disabled#stream-resolution td Resolution: - td #[select(disabled id="stream-resolution-selector")] - tr(id="stream-quality" class="feature-disabled") - td JPEG quality: - td(class="value-slider") #[input(disabled type="range" id="stream-quality-slider" class="slider")] - td(id="stream-quality-value" class="value-number") + td #[select#stream-resolution-selector(disabled)] + tr.feature-disabled#stream-quality + +menu_slider_td3("stream-quality-slider", "stream-quality-value", false) JPEG quality: tr - td JPEG max fps: - td(class="value-slider") #[input(disabled type="range" id="stream-desired-fps-slider" class="slider")] - td(id="stream-desired-fps-value" class="value-number") - tr(id="stream-h264-bitrate" class="feature-disabled") - td H.264 kbps: - td(class="value-slider") #[input(disabled type="range" id="stream-h264-bitrate-slider" class="slider")] - td(id="stream-h264-bitrate-value" class="value-number") - tr(id="stream-h264-gop" class="feature-disabled") - td H.264 #[a(target="_blank" href="https://docs.pikvm.org/webrtc") gop]: - td(class="value-slider") #[input(disabled type="range" id="stream-h264-gop-slider" class="slider")] - td(id="stream-h264-gop-value" class="value-number") - tr(id="stream-mode" class="feature-disabled") - td Video #[a(target="_blank" href="https://docs.pikvm.org/webrtc") mode]: - td - div(class="radio-box") - input(type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus") - label(for="stream-mode-radio-janus") WebRTC - input(type="radio" id="stream-mode-radio-media" name="stream-mode-radio" value="media") - label(for="stream-mode-radio-media") H.264 - input(checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg") - label(for="stream-mode-radio-mjpeg") MJPEG - tr(id="stream-orient" class="feature-disabled") - td Orientation: - td - div(class="radio-box") - input(checked type="radio" id="stream-orient-radio-0" name="stream-orient-radio" value="0") - label(for="stream-orient-radio-0") Default - input(type="radio" id="stream-orient-radio-90" name="stream-orient-radio" value="90") - label(for="stream-orient-radio-90") 90° - input(type="radio" id="stream-orient-radio-180" name="stream-orient-radio" value="180") - label(for="stream-orient-radio-180") 180° - input(type="radio" id="stream-orient-radio-270" name="stream-orient-radio" value="270") - label(for="stream-orient-radio-270") 270° - tr(id="stream-audio" class="feature-disabled") - td Audio volume: - td(class="value-slider") #[input(type="range" id="stream-audio-volume-slider" class="slider")] - td(id="stream-audio-volume-value" class="value-number") - tr(id="stream-mic" class="feature-disabled") - +menu_switch_notable("stream-mic-switch", "Microphone", false, false) + +menu_slider_td3("stream-desired-fps-slider", "stream-desired-fps-value", false) JPEG max fps: + tr.feature-disabled#stream-h264-bitrate + +menu_slider_td3("stream-h264-bitrate-slider", "stream-h264-bitrate-value", false) H.264 kbps: + tr.feature-disabled#stream-h264-gop + +menu_slider_td3("stream-h264-gop-slider", "stream-h264-gop-value", false) + |H.264 #[a(target="_blank" href="https://docs.pikvm.org/webrtc") gop]: + tr.feature-disabled#stream-mode + +menu_radio_td2("stream-mode-radio", [ + {title: "WebRTC", value: "janus"}, + {title: "H.264", value: "media"}, + {title: "MJPEG", value: "mjpeg", checked: true}, + ]) #[a(target="_blank" href="https://docs.pikvm.org/webrtc") Video mode]: + tr.feature-disabled#stream-orient + +menu_radio_td2("stream-orient-radio", [ + {title: "Default", value: "0", checked: true}, + {title: "90°", value: "90"}, + {title: "180°", value: "180"}, + {title: "270°", value: "270"}, + ]) Orientation: + tr.feature-disabled#stream-audio + +menu_slider_td3("stream-audio-volume-slider", "stream-audio-volume-value") Audio volume: + tr.feature-disabled#stream-mic + +menu_switch_td2("stream-mic-switch", false, false) Microphone: hr - div(class="buttons buttons-row") - button(data-force-hide-menu data-show-window="stream-window" class="row33") • Show stream - button(data-force-hide-menu id="stream-screenshot-button" class="row33") • Screenshot - button(id="stream-reset-button" class="row33") Reset stream + + .buttons.buttons-row + button.row33(data-force-hide-menu data-show-window="stream-window") • Show stream + button.row33#stream-screenshot-button(data-force-hide-menu) • Screenshot + button.row33#stream-reset-button Reset stream hr - table(class="kv") - tr(id="hid-outputs-keyboard", class="feature-disabled") + + table.kv + tr.feature-disabled#hid-outputs-keyboard td Keyboard mode: - td #[div(id="hid-outputs-keyboard-box" class="radio-box")] - tr(id="hid-outputs-mouse", class="feature-disabled") - td Mouse #[a(target="_blank" href="https://docs.pikvm.org/mouse") mode]: - td #[div(id="hid-outputs-mouse-box" class="radio-box")] - details - summary Keyboard & mouse (HID) settings - div(class="spoiler") - table(class="kv") - tr - +menu_switch_notable("hid-keyboard-swap-cc-switch", "Swap Left Ctrl and Caps keys", true, false) - hr - table(class="kv") - tr - td Mouse polling: - td(class="value-slider") #[input(type="range" id="hid-mouse-rate-slider" class="slider")] - td(id="hid-mouse-rate-value" class="value-number") - tr(id="hid-mouse-sens" class="feature-disabled") - td Relative sensitivity: - td(class="value-slider") #[input(disabled type="range" id="hid-mouse-sens-slider" class="slider")] - td(id="hid-mouse-sens-value" class="value-number") - tr(id="hid-mouse-squash" class="feature-disabled") - +menu_switch_notable("hid-mouse-squash-switch", "Squash relative moves", true, true) - tr - td Reverse scrolling: - td - table - tr - +menu_switch_notable("hid-mouse-reverse-scrolling-switch", "Y", true, false) - td    - +menu_switch_notable("hid-mouse-reverse-panning-switch", "X", true, false) - tr - +menu_switch_notable("hid-mouse-cumulative-scrolling-switch", "Cumulative scrolling", true, false) - tr - td Scroll rate: - td(class="value-slider") #[input(type="range" id="hid-mouse-scroll-slider" class="slider")] - td(id="hid-mouse-scroll-value" class="value-number") - tr - +menu_switch_notable("hid-mouse-dot-switch", "Show the blue dot", true, true) - details - summary Web UI settings - div(class="spoiler") - table(class="kv") - tr - +menu_switch_notable("page-close-ask-switch", "Ask page close confirmation", true, true) - tr - +menu_switch_notable("page-full-tab-stream-switch", "Expand for the entire tab by default", true, false) - table(class="kv") + td #[div.radio-box#hid-outputs-keyboard-box] + tr.feature-disabled#hid-outputs-mouse + td #[a(target="_blank" href="https://docs.pikvm.org/mouse") Mouse mode]: + td #[div.radio-box#hid-outputs-mouse-box] + + +menu_spoiler("Keyboard & mouse (HID) settings") + +menu_switch_table("hid-keyboard-swap-cc-switch", true, false) Swap Left Ctrl and Caps keys: + hr + table(class="kv") + tr + +menu_slider_td3("hid-mouse-rate-slider", "hid-mouse-rate-value") Mouse polling: + tr.feature-disabled#hid-mouse-sens + +menu_slider_td3("hid-mouse-sens-slider", "hid-mouse-sens-value", false) Relative sensitivity: + tr(id="hid-mouse-squash" class="feature-disabled") + +menu_switch_td2("hid-mouse-squash-switch", true, true) Squash relative moves: + tr + td Reverse scrolling: + td + table + tr + +menu_switch_td2("hid-mouse-reverse-scrolling-switch", true, false) Y: + td    + +menu_switch_td2("hid-mouse-reverse-panning-switch", true, false) X: + tr + +menu_switch_td2("hid-mouse-cumulative-scrolling-switch", true, false) Cumulative scrolling: + tr + +menu_slider_td3("hid-mouse-scroll-slider", "hid-mouse-scroll-value") Scroll rate: + tr + +menu_switch_td2("hid-mouse-dot-switch", true, true) Show the blue dot: + + +menu_spoiler("Web UI settings") + table.kv + tr + +menu_switch_td2("page-close-ask-switch", true, true) Ask page close confirmation: + tr + +menu_switch_td2("page-full-tab-stream-switch", true, false) Expand for the entire tab by default: + + table.kv tr - +menu_switch_notable("hid-keyboard-bad-link-switch", "Bad link mode (release keys immediately)", true, false) - tr(id="hid-connect" class="feature-disabled") - +menu_switch_notable("hid-connect-switch", "Connect HID to Server", true, true) - tr(id="hid-jiggler" class="feature-disabled") - +menu_switch_notable("hid-jiggler-switch", "Mouse jiggler", false, false) + +menu_switch_td2("hid-keyboard-bad-link-switch", true, false) Bad link mode (release keys immediately): + tr.feature-disabled#hid-connect + +menu_switch_td2("hid-connect-switch", true, true) Connect HID to Server: + tr.feature-disabled#hid-jiggler + +menu_switch_td2("hid-jiggler-switch", false, false) + a(target="_blank" href="https://docs.pikvm.org/mouse_jiggler") Mouse jiggler: tr - +menu_switch_notable("hid-mute-switch", "Mute all input HID events", true, false) - tr(id="v3-usb-breaker" class="feature-disabled") - +menu_switch_notable_gpio("__v3_usb_breaker__", "Connect main USB to Server", - "Turning off this switch will disconnect the main USB from the server. Are you sure you want to continue?") - tr(id="v4-locator" class="feature-disabled") - +menu_switch_notable_gpio("__v4_locator__", "Enable locator LED") + +menu_switch_td2("hid-mute-switch", true, false) Mute all input HID events: + tr.feature-disabled#v3-usb-breaker + +menu_switch_td2_gpio( + "__v3_usb_breaker__", + "Turning off this switch will disconnect the main USB from the server. Are you sure you want to continue?" + ) Connect main USB to Server: + tr.feature-disabled#v4-locator + +menu_switch_td2_gpio("__v4_locator__") Enable locator LED: hr - div(class="buttons buttons-row") - button(data-force-hide-menu data-show-window="keyboard-window" class="row50") • Show keyboard - button(disabled id="hid-reset-button" class="row50") Reset HID + + .buttons.buttons-row + button.row50(data-force-hide-menu data-show-window="keyboard-window") • Show keyboard + button.row50#hid-reset-button(disabled) Reset HID diff --git a/web/kvm/navbar-text.pug b/web/kvm/navbar-text.pug index e4a37919..499072eb 100644 --- a/web/kvm/navbar-text.pug +++ b/web/kvm/navbar-text.pug @@ -1,45 +1,43 @@ -li(id="text-dropdown" class="right") - a(class="menu-button" href="#") +li.right#text-dropdown + a.menu-button(href="#") +navbar_led("stream-ocr-led", "led-gear", "feature-disabled") span Text - div(id="text-menu" class="menu") - div(class="text") + + .menu#text-menu + .text b Paste text as keypress sequence#[br] sub Please note that PiKVM cannot switch the keyboard layout hr - div(class="text" style="margin-right: 20px") - textarea(id="hid-pak-text" data-focus placeholder="Enter your text here") - table(class="kv") + .text(style="margin-right: 20px") + textarea#hid-pak-text(data-focus placeholder="Enter your text here") + table.kv tr - td - button(disabled data-force-hide-menu id="hid-pak-button") • Paste + td #[button#hid-pak-button(disabled data-force-hide-menu) • Paste] td using host keymap - td - select(id="hid-pak-keymap-selector") - table(class="kv") + td #[select#hid-pak-keymap-selector] + table.kv tr - +menu_switch_notable("hid-pak-slow-switch", "Slow typing", true, false) + +menu_switch_td2("hid-pak-slow-switch", true, false) Slow typing: tr - +menu_switch_notable("hid-pak-secure-switch", "Hide input text", true, false) + +menu_switch_td2("hid-pak-secure-switch", true, false) Hide input text: tr - +menu_switch_notable("hid-pak-ask-switch", "Ask paste confirmation", true, true) - div(id="stream-ocr" class="feature-disabled") + +menu_switch_td2("hid-pak-ask-switch", true, true) Ask paste confirmation: + + .feature-disabled#stream-ocr hr br hr - div(class="text") + .text b Text recognition β#[br] sub #[a(target="_blank" href="https://docs.pikvm.org/ocr") OCR] works locally on PiKVM hr - table(class="kv") + table.kv tr - td - button(data-force-hide-menu id="stream-ocr-button") • Select area + td #[button#stream-ocr-button(data-force-hide-menu) • Select area] td for - td - select(id="stream-ocr-lang-selector") + td #[select#stream-ocr-lang-selector] td text recognition - table(class="kv") + table.kv tr td(colspan="4") • Press #[b Enter] to recognize and copy text to clipboard tr diff --git a/web/kvm/navbar.pug b/web/kvm/navbar.pug index a9189b7d..a0aeaa2b 100644 --- a/web/kvm/navbar.pug +++ b/web/kvm/navbar.pug @@ -1,8 +1,9 @@ mixin navbar_led(id, icon, cls="led-gray") img(id=id, class=cls src=`${svg_dir}/${icon}.svg`) + mixin menu_message(icon, short, classes="") - div(class="text") + .text table tr td(rowspan="2") #[img(class=`sign ${classes}` src=`${svg_dir}/${icon}.svg`)] @@ -13,34 +14,77 @@ mixin menu_message(icon, short, classes="") sup(style="line-height:1") block -mixin menu_switch_notable_gpio(channel, title, confirm_off="") - td !{title}: - td(align="right") - div(class="switch-box") - input(disabled type="checkbox" id=`__gpio-switch-${channel}` class=`__gpio-switch-${channel} gpio-switch` - data-channel=channel data-confirm-off=confirm_off) - label(for=`__gpio-switch-${channel}`) - span(class="switch-inner") - span(class="switch") -mixin menu_switch_notable(id, title, enabled, checked) - td !{title}: +mixin menu_switch_td2_gpio(channel, confirm_off="") + td + block td(align="right") - div(class="switch-box") + .switch-box + input( + disabled + type="checkbox" + id=`__gpio-switch-${channel}` + class=`__gpio-switch-${channel} gpio-switch` + data-channel=channel + data-confirm-off=confirm_off + ) + label(for=`__gpio-switch-${channel}`) + span.switch-inner + span.switch + + +mixin menu_switch_td2(id, enabled, checked) + td + block + td(align="right") + .switch-box input(checked=checked disabled=!enabled type="checkbox" id=id) label(for=id) - span(class="switch-inner") - span(class="switch") + span.switch-inner + span.switch -mixin menu_switch(id, title, enabled, checked) - table(class="kv") + +mixin menu_switch_table(id, enabled, checked) + table.kv tr - +menu_switch_notable(id, title, enabled, checked) + +menu_switch_td2(id, enabled, checked) + block -ul(id="navbar") - li(class="left") + +mixin menu_radio_td2(name, items) + td + block + td + .radio-box + each item in items + - + let id = `${name}-${item["value"]}` + let checked = (item["checked"] || false) + input(type="radio" id=id name=name value=item["value"] checked=checked) + label(for=id) !{item["title"]} + + +mixin menu_slider_td3(slider_id, value_id, enabled) + - + enabled = (enabled || true) + td + block + td.value-slider + input.slider(type="range" id=slider_id disabled=!enabled) + td.value-number(id=value_id) + + +mixin menu_spoiler(title) + details + summary !{title} + div(class="spoiler") + block + + +ul#navbar + li.left a(id="logo" href="/") ←   - img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="π-kvm") + img.svg-gray(src=`${svg_dir}/logo.svg` alt="π-kvm") include navbar-health.pug diff --git a/web/kvm/window-about.pug b/web/kvm/window-about.pug index d8bc8c2e..0c717aa3 100644 --- a/web/kvm/window-about.pug +++ b/web/kvm/window-about.pug @@ -2,47 +2,49 @@ mixin about_tab(name, title, checked=false) - let button_id = `about-tab-${name}-button` input(checked=checked type="radio" name="about-tab-button", id=button_id) label(for=button_id) #{title} - div(class="tab") - div(id=`about-${name}` class="code") + .tab + .code(id=`about-${name}`) if block block else - span(class="code-comment") No data + span.code-comment No data -div(id="about-window" class="window") - div(class="window-header") - div(class="window-grab") About - button(class="window-button-close") #[b ×] - div(id="about") +.window#about-window + .window-header + .window-grab About + button.window-button-close #[b ×] + + #about table tr - td(class="logo") + td.logo a(href="https://pikvm.org" target="_blank") - img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="PiKVM" height="40") + img.svg-gray(src=`${svg_dir}/logo.svg` alt="PiKVM" height="40") td table - tr #[td(colspan="2" class="title") The Open Source KVM over IP] tr - td(colspan="2" class="copyright") + td.title(colspan="2") + | The Open Source KVM over IP + tr + td.copyright(colspan="2") | Copyright © 2018-2024 #[a(target="_blank" href="mailto:mdevaev@gmail.com") Maxim Devaev] br - div(class="tabs-box") + .tabs-box +about_tab("meta", "Meta", true) div - span(class="code-comment") + span.code-comment | // You can get this JSON using handle #[a(target="_blank" href="/api/info?fields=meta") /api/info?fields=meta]#[br] | // In the standard configuration this data#[br] | // is specified in the file /etc/kvmd/meta.yaml br - pre(id="kvmd-meta-json") - | No data + pre#kvmd-meta-json No data +about_tab("hardware", "Hardware") +about_tab("version", "Version") +about_tab("thanks", "Thanks") - span(class="code-comment") + span.code-comment | // These kind people donated money to the PiKVM project#[br] | // and supported the work on it. We are very grateful#[br] | // for their help, and memorializing their names#[br] @@ -690,7 +692,7 @@ div(id="about-window" class="window") li Zoltan Magyari li Zsombor Vari br - p(class="text credits") + p.text.credits a(target="_blank" href="https://pikvm.org") PiKVM Project |   |   a(target="_blank" href="https://docs.pikvm.org") Documentation diff --git a/web/kvm/window-keyboard.pug b/web/kvm/window-keyboard.pug index ae1a1e1f..34921f95 100644 --- a/web/kvm/window-keyboard.pug +++ b/web/kvm/window-keyboard.pug @@ -1,41 +1,43 @@ -mixin key(spacer, code, classes="", width=0) - div(data-code=code, class=`key ${classes}`, style=(width ? `width:${width}px` : "")) - div(class="label") - block - if spacer == 1 - div(class="spacer") - else if spacer == 2 - div(class="spacer-fixed") +mixin spacer(sp) + if sp == 1 + .spacer + else if sp == 2 + .spacer-fixed -mixin modifier(spacer, code, classes="", width=0) - div(data-code=code class=`modifier ${classes}` style=(width ? `width:${width}px` : "")) - div(class="label") + +mixin key(sp, code, classes="", width=0) + div(data-code=code, class=`key ${classes}`, style=(width ? `width: ${width}px` : "")) + .label + block + +spacer(sp) + + +mixin modifier(sp, code, classes="", width=0) + div(data-code=code class=`modifier ${classes}` style=(width ? `width: ${width}px` : "")) + .label | #[b •]#[br] block - if spacer == 1 - div(class="spacer") - else if spacer == 2 - div(class="spacer-fixed") + +spacer(sp) -mixin empty(spacer, classes="", width=0) + +mixin empty(sp, classes="", width=0) div(class=`empty ${classes}` style=(width ? `width:${width}px` : "")) - div(class="label")   - if spacer == 1 - div(class="spacer") - else if spacer == 2 - div(class="spacer-fixed") + .label   + +spacer(sp) + mixin lamp(cls) img(class=`inline-lamp-small ${cls} led-gray` src=`${svg_dir}/led-square.svg`) -div(id="keyboard-window" class="window") - div(id="keyboard-window-header" class="window-header") - div(class="window-grab") Virtual Keyboard - button(class="window-button-close") #[b ×] - div(id="keyboard-desktop" class="keypad" align="center") - div(class="keypad-block") - div(class="keypad-row") +.window#keyboard-window + .window-header#keyboard-window-header + .window-grab Virtual Keyboard + button.window-button-close #[b ×] + + .keypad#keyboard-desktop(align="center") + .keypad-block + .keypad-row +key(2, "Escape", "small") Esc +empty(1, "", 24) each key in ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] @@ -43,7 +45,7 @@ div(id="keyboard-window" class="window") if key == "F4" || key == "F8" +empty(1, "", 10) hr - div(class="keypad-row") + .keypad-row +key(1, "Backquote") ~#[br]` each key, index in ["!", "@", "#", "$", "%", "^", "&", "*", "("] +key(1, `Digit${index + 1}`) #{key}#[br]#{index + 1} @@ -51,14 +53,14 @@ div(id="keyboard-window" class="window") +key(1, "Minus") _#[br]- +key(1, "Equal") +#[br]= +key(0, "Backspace", "wide-1 right") ↤ - div(class="keypad-row") + .keypad-row +key(1, "Tab", "wide-1 left") ⇤#[br]⇥ each key in ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"] +key(1, `Key${key}`, "single") #{key} +key(1, "BracketLeft") {#[br][ +key(1, "BracketRight") }#[br]] +key(0, "Backslash") |#[br]\ - div(class="keypad-row") + .keypad-row +key(1, "CapsLock", "wide-2 left small") +lamp("hid-keyboard-caps-led") | #[br] Caps Lock @@ -67,7 +69,7 @@ div(id="keyboard-window" class="window") +key(1, "Semicolon") :#[br]; +key(1, "Quote") "#[br]' +key(0, "Enter", "wide-2 right small") Enter#[br]↵ - div(class="keypad-row") + .keypad-row +modifier(1, "ShiftLeft", "wide-3 left small") Shift each key in ["Z", "X", "C", "V", "B", "N", "M"] +key(1, `Key${key}`, "single") #{key} @@ -75,7 +77,7 @@ div(id="keyboard-window" class="window") +key(1, "Period") >#[br]. +key(1, "Slash") ?#[br]/ +modifier(0, "ShiftRight", "wide-3 right small") Shift - div(class="keypad-row") + .keypad-row +modifier(2, "ControlLeft", "wide-1 left small") Ctrl +modifier(2, "MetaLeft", "wide-1 left small") Win +modifier(2, "AltLeft", "wide-1 left small") Alt @@ -84,83 +86,83 @@ div(id="keyboard-window" class="window") +modifier(2, "MetaRight", "wide-1 right small") Win +key(2, "ContextMenu", "small") #[br]Menu +modifier(0, "ControlRight", "wide-1 right small") Ctrl - div(class="keypad-block") - div(class="keypad-row") + .keypad-block + .keypad-row +modifier(2, "PrintScreen", "small") Pt/Sq +key(2, "ScrollLock", "small") +lamp("hid-keyboard-scroll-led") | #[br] ScrLk +key(0, "Pause", "small") P/Brk hr - div(class="keypad-row") + .keypad-row +key(2, "Insert", "small") Ins +key(2, "Home", "small") Home +key(0, "PageUp", "small") PgUp - div(class="keypad-row") + .keypad-row +key(2, "Delete", "small") Del +key(2, "End", "small") End +key(0, "PageDown", "small") PgDn - div(class="keypad-row") - div(class="keypad-row") + .keypad-row + .keypad-row +empty(1, "") +key(2, "ArrowUp") ↑ +empty(0, "") - div(class="keypad-row") + .keypad-row +key(2, "ArrowLeft") ← +key(2, "ArrowDown") ↓ +key(0, "ArrowRight") → - div(class="keypad-block") - div(class="keypad-row") + .keypad-block + .keypad-row +empty(2, "small") +empty(2, "small") +empty(2, "small") +key(0, "Power", "small") PWR hr - div(class="keypad-row") + .keypad-row +key(2, "NumLock", "small") +lamp("hid-keyboard-num-led") | #[br] NmLk +key(2, "NumpadDivide") / +key(2, "NumpadMultiply") * +key(0, "NumpadSubtract") - - div(class="keypad-row") + .keypad-row +key(2, "Numpad7", "small") 7#[br]Home +key(2, "Numpad8", "small") 8#[br]↑ +key(2, "Numpad9", "small") 9#[br]PgUp +empty(0, "") - div(class="keypad-row") + .keypad-row +key(2, "Numpad4", "small") 4#[br]← +key(2, "Numpad5", "small") 5#[br]#[br] +key(2, "Numpad6", "small") 6#[br]→ +key(0, "NumpadAdd") + - div(class="keypad-row") + .keypad-row +key(2, "Numpad1", "small") 1#[br]End +key(2, "Numpad2", "small") 2#[br]↓ +key(2, "Numpad3", "small") 3#[br]PgDn +empty(0, "") - div(class="keypad-row") + .keypad-row +key(2, "Numpad0", "small") 0#[br]Ins +empty(2, "") +key(2, "NumpadDecimal", "small") .#[br]Del +key(0, "NumpadEnter", "small") Ent - div(class="keypad-block") - div(class="keypad-row") + .keypad-block + .keypad-row +key(0, "IntlBackslash", "small") \#[br]| hr - div(class="keypad-row") + .keypad-row +key(0, "IntlYen", "small") ¥#[br]_ - div(class="keypad-row") + .keypad-row +key(0, "IntlRo", "small") \#[br]ろ - div(class="keypad-row") + .keypad-row +modifier(0, "KanaMode", "small") Kana - div(class="keypad-row") + .keypad-row +modifier(0, "NonConvert", "small") N/Cnv - div(class="keypad-row") + .keypad-row +modifier(0, "Convert", "small") Cnv - div(id="keyboard-mobile" class="keypad" align="center") - div(class="keypad-block") - div(class="keypad-row") + .keypad#keyboard-mobile(align="center") + .keypad-block + .keypad-row +key(1, "Escape", "small") Esc +key(0, "F1", "wide-0 small rounded-left") F1 +key(0, "F2", "wide-0 small rounded-none") F2 @@ -183,7 +185,7 @@ div(id="keyboard-window" class="window") +key(1, "Home", "small") Home +key(1, "End", "small") End +key(0, "Delete", "small") Del - div(class="keypad-row") + .keypad-row +key(1, "Backquote") ~#[br]` each key, index in ["!", "@", "#", "$", "%", "^", "&", "*", "("] +key(1, `Digit${index + 1}`) #{key}#[br]#{index + 1} @@ -191,14 +193,14 @@ div(id="keyboard-window" class="window") +key(1, "Minus") _#[br]- +key(1, "Equal") +#[br]= +key(0, "Backspace", "wide-2 right", 101) ↤ - div(class="keypad-row") + .keypad-row +key(1, "Tab", "wide-1 left") ⇤
    ⇥ each key in ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"] +key(1, `Key${key}`, "single") #{key} +key(1, "BracketLeft") {#[br][ +key(1, "BracketRight") }#[br]] +key(0, "Backslash", "wide-1 left", 78) |#[br]\ - div(class="keypad-row") + .keypad-row +key(1, "CapsLock", "wide-2 left small") +lamp("hid-keyboard-caps-led") | #[br] Caps Lock @@ -207,7 +209,7 @@ div(id="keyboard-window" class="window") +key(1, "Semicolon") :#[br]; +key(1, "Quote") `#[br]' +key(0, "Enter", "wide-3 right small", 116) Enter#[br]↵ - div(class="keypad-row") + .keypad-row +modifier(1, "ShiftLeft", "wide-3 left small") Shift each key in ["Z", "X", "C", "V", "B", "N", "M"] +key(1, `Key${key}`, "single") #{key} @@ -217,7 +219,7 @@ div(id="keyboard-window" class="window") +key(2, "PageUp", "small") PgUp +key(2, "ArrowUp") ↑ +key(0, "PageDown", "small") PgDn - div(class="keypad-row") + .keypad-row +modifier(1, "ControlLeft", "wide-1 left small") Ctrl +modifier(1, "MetaLeft", "wide-1 left small") Win +modifier(1, "AltLeft", "wide-1 left small") Alt diff --git a/web/kvm/window-stream.pug b/web/kvm/window-stream.pug index cbe998e3..4fc3c42f 100644 --- a/web/kvm/window-stream.pug +++ b/web/kvm/window-stream.pug @@ -1,49 +1,50 @@ -div(id="stream-ocr-window" class="window") - div(id="stream-ocr-selection" class="hidden") +.window#stream-ocr-window + .hidden#stream-ocr-selection -div(id="stream-window" class="window window-resizable") - div(id="stream-window-header" class="window-header") - div(class="window-grab") MJPEG - button(class="window-button-close") #[b ×] - button(class="window-button-maximize") ☐ - button(class="window-button-original") • - button(class="window-button-enter-full-tab") ▲ - button(class="window-button-full-screen") ⤢ +.window.window-resizable#stream-window + .window-header#stream-window-header + .window-grab MJPEG + button.window-button-close #[b ×] + button.window-button-maximize ☐ + button.window-button-original • + button.window-button-enter-full-tab ▲ + button.window-button-full-screen ⤢ - div(id="stream-info") + #stream-info - button(class="window-button-exit-full-tab") ▼ - div(id="stream-box" class="stream-box-offline") - img(id="stream-image" src=`${png_dir}/blank-stream.png`) - video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted) - canvas(id="stream-canvas" class="hidden") - div(id="stream-fullscreen-active") + button.window-button-exit-full-tab ▼ - div(id="stream-mouse-buttons" class="keypad" align="center") - div(class="keypad-block") - div(class="keypad-row") - div(data-code="left" class="key wide-3 left rounded-left") - div(class="label") Left - div(data-code="left" class="modifier left small rounded-right") - div(class="label") #[b •]#[br]Hold + .stream-box-offline#stream-box + img#stream-image(src=`${png_dir}/blank-stream.png`) + video.hidden#stream-video(disablePictureInPicture="true" autoplay playsinline muted) + canvas.hidden#stream-canvas + #stream-fullscreen-active - div(class="empty" style="width:15px") + .keypad#stream-mouse-buttons(align="center") + .keypad-block + .keypad-row + .key.wide-3.left.rounded-left(data-code="left") + .label Left + .modifier.left.small.rounded-right(data-code="left") + .label #[b •]#[br]Hold - div(data-code="middle" class="key wide-1 left rounded-left") - div(class="label") Mid - div(data-code="middle" class="modifier left small rounded-right") - div(class="label") #[b •]#[br]Hold + .empty(style="width: 15px") - div(class="empty" style="width:15px") + .key.wide-1.left.rounded-left(data-code="middle") + .label Mid + .modifier.left.small.rounded-right(data-code="middle") + .label #[b •]#[br]Hold - div(data-code="right" class="modifier right small rounded-left") - div(class="label") #[b •]#[br]Hold - div(data-code="right" class="key wide-3 right rounded-right") - div(class="label") Right + .empty(style="width: 15px") - div(class="empty" style="width:30px") + .modifier.right.small.rounded-left(data-code="right") + .label #[b •]#[br]Hold + .key.wide-3.right.rounded-right(data-code="right") + .label Right - div(data-code="up" class="key small rounded-left") - div(class="label") Up - div(data-code="down" class="key small rounded-right") - div(class="label") Down + .empty(style="width: 30px") + + .key.small.rounded-left(data-code="up") + .label Up + .key.small.rounded-right(data-code="down") + .label Down diff --git a/web/kvm/window-switch.pug b/web/kvm/window-switch.pug index 71c0e152..7317ce6c 100644 --- a/web/kvm/window-switch.pug +++ b/web/kvm/window-switch.pug @@ -1,95 +1,68 @@ mixin switch_tab(name, title, checked=false) - let button_id = `switch-tab-${name}-button` - input(checked=checked type="radio" name="switch-tab-button", id=button_id) + input(checked=checked type="radio" name="switch-tab-button" id=button_id) label(for=button_id) #{title} - div(class="tab") + .tab block -div(id="switch-window" class="window" style="width:min-content") - div(class="window-header") - div(class="window-grab") Switch settings - button(class="window-button-close") #[b ×] - div(class="tabs-box") +mixin color_slider_tr(name, title) + tr + td(style="white-space: nowrap") #{title}: + td #[input(type="color" id=`switch-color-${name}-input`)] + td #[input(type="range" id=`switch-color-${name}-brightness-slider` style="min-width: 150px")] + td     + td #[button(id=`switch-color-${name}-default-button` class="small" title="Reset default") ↻] + + +.window#switch-window(style="width: min-content") + .window-header + .window-grab Switch settings + button.window-button-close #[b ×] + + .tabs-box +switch_tab("edid", "EDIDs collection", true) table tr td(colspan="2") - select(id="switch-edid-selector" size="8") - td(rowspan="2" style="vertical-align:top") - table(class="kv") + select#switch-edid-selector(size="8") + td(rowspan="2" style="vertical-align: top") + table.kv tr td Manufacturer: - td(id="switch-edid-info-mfc-id" class="value") + td.value#switch-edid-info-mfc-id tr td Product ID: - td(id="switch-edid-info-product-id" class="value") + td.value#switch-edid-info-product-id tr td Serial: - td(id="switch-edid-info-serial" class="value") + td.value#switch-edid-info-serial tr td Monitor name: - td(id="switch-edid-info-monitor-name" class="value") + td.value#switch-edid-info-monitor-name tr td Extra serial: - td(id="switch-edid-info-monitor-serial" class="value") + td.value#switch-edid-info-monitor-serial tr td Audio enabled: - td(id="switch-edid-info-audio" class="value") + td.value#switch-edid-info-audio tr td Data: - td #[button(disabled id="switch-edid-copy-data-button" class="small") Copy] + td #[button.small#switch-edid-copy-data-button(disabled) Copy] tr - td #[button(id="switch-edid-add-button") Add new] - td(style="float:right") #[button(disabled id="switch-edid-remove-button") Remove] + td #[button#switch-edid-add-button Add new] + td(style="float: right") #[button#switch-edid-remove-button(disabled) Remove] +switch_tab("colors", "Color scheme") table - //tr - td Role - td Color - td Brightness - td - td Reset - //tr - td #[hr] - td #[hr] - td #[hr] - td - td #[hr] - tr - td(style="white-space: nowrap") Selected port: - td #[input(type="color" id="switch-color-active-input")] - td #[input(type="range" id="switch-color-active-brightness-slider" style="min-width:150px")] - td     - td #[button(id="switch-color-active-default-button" class="small" title="Reset default") ↻] - tr - td(style="white-space: nowrap") Inactive port: - td #[input(type="color" id="switch-color-inactive-input")] - td #[input(type="range" id="switch-color-inactive-brightness-slider" style="min-width:150px")] - td     - td #[button(id="switch-color-inactive-default-button" class="small" title="Reset default") ↻] - tr - td(style="white-space: nowrap") Blinking beacon: - td #[input(type="color" id="switch-color-beacon-input")] - td #[input(type="range" id="switch-color-beacon-brightness-slider" style="min-width:150px")] - td     - td #[button(id="switch-color-beacon-default-button" class="small" title="Reset default") ↻] + +color_slider_tr("active", "Selected port") + +color_slider_tr("inactive", "Inactive port") + +color_slider_tr("beacon", "Blinking beacon") tr td #[hr] td #[hr] td #[hr] td td #[hr] - tr - td(style="white-space: nowrap") Flashing downlink: - td #[input(type="color" id="switch-color-flashing-input")] - td #[input(type="range" id="switch-color-flashing-brightness-slider" style="min-width:150px")] - td     - td #[button(id="switch-color-flashing-default-button" class="small" title="Reset default") ↻] - tr - td(style="white-space: nowrap") Bootloader mode: - td #[input(type="color" id="switch-color-bootloader-input")] - td #[input(type="range" id="switch-color-bootloader-brightness-slider" style="min-width:150px")] - td     - td #[button(id="switch-color-bootloader-default-button" class="small" title="Reset default") ↻] + +color_slider_tr("flashing", "Flashing downlink") + +color_slider_tr("bootloader", "Bootloader mode") diff --git a/web/kvm/window-webterm.pug b/web/kvm/window-webterm.pug index 3cb9b9c7..b5164b3f 100644 --- a/web/kvm/window-webterm.pug +++ b/web/kvm/window-webterm.pug @@ -1,8 +1,9 @@ -div(id="webterm-window" class="window window-resizable" style="width: 640px; height: 480px") - div(class="window-header") - div(class="window-grab") Terminal - button(class="window-button-close") #[b ×] - button(class="window-button-maximize") ☐ +.window.window-resizable#webterm-window(style="width: 640px; height: 480px") + .window-header + .window-grab Terminal + button.window-button-close #[b ×] + button.window-button-maximize ☐ // Терминал глючит из-за зажимаемой клавиши ESC для выхода // button(class="window-button-full-screen") ⤢ - iframe(id="webterm-iframe" src="" style="width: 100%; height: 100%") + + iframe#webterm-iframe(src="" style="width: 100%; height: 100%") diff --git a/web/login/index.html b/web/login/index.html index 90a840dd..ccf21006 100644 --- a/web/login/index.html +++ b/web/login/index.html @@ -51,19 +51,19 @@
    Username:  - +
    Password:  - +
    2FA code:  - +
    - +
    Direct HTTP H.264 streaming is not supportedDirect H.264 streaming is not supported
    @@ -203,7 +203,7 @@
    JPEG quality: - +
    JPEG max fps: - +
    H.264 kbps: - +
    H.264 gop: - +
    - + - + - +
    Audio volume: - +
    Mouse polling: - +
    Relative sensitivity: - +
    Scroll rate: - +
    Mouse jiggler: + Mouse jiggler:
    diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index 36256edb..a750e0cf 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -30,7 +30,7 @@ li.right#system-dropdown +menu_message("warning", "WebRTC is not supported by this browser") hr .hidden#stream-message-no-vd - +menu_message("warning", "Direct HTTP H.264 streaming is not supported") + +menu_message("warning", "Direct H.264 streaming is not supported") hr .hidden#stream-message-no-h264 +menu_message("warning", "H.264 is not supported by this browser") @@ -51,9 +51,9 @@ li.right#system-dropdown |H.264 #[a(target="_blank" href="https://docs.pikvm.org/webrtc") gop]: tr.feature-disabled#stream-mode +menu_radio_td2("stream-mode-radio", [ - {title: "WebRTC", value: "janus"}, - {title: "H.264", value: "media"}, - {title: "MJPEG", value: "mjpeg", checked: true}, + {title: "WebRTC
    H.264", value: "janus"}, + {title: "Direct
    H.264", value: "media"}, + {title: "Legacy
    MJPEG", value: "mjpeg", checked: true}, ]) #[a(target="_blank" href="https://docs.pikvm.org/webrtc") Video mode]: tr.feature-disabled#stream-orient +menu_radio_td2("stream-orient-radio", [ @@ -121,7 +121,7 @@ li.right#system-dropdown +menu_switch_td2("hid-connect-switch", true, true) Connect HID to Server: tr.feature-disabled#hid-jiggler +menu_switch_td2("hid-jiggler-switch", false, false) - a(target="_blank" href="https://docs.pikvm.org/mouse_jiggler") Mouse jiggler: + | #[a(target="_blank" href="https://docs.pikvm.org/mouse_jiggler") Mouse jiggler]: tr +menu_switch_td2("hid-mute-switch", true, false) Mute all input HID events: tr.feature-disabled#v3-usb-breaker diff --git a/web/kvm/navbar.pug b/web/kvm/navbar.pug index a0aeaa2b..4ea9812a 100644 --- a/web/kvm/navbar.pug +++ b/web/kvm/navbar.pug @@ -70,7 +70,7 @@ mixin menu_slider_td3(slider_id, value_id, enabled) td block td.value-slider - input.slider(type="range" id=slider_id disabled=!enabled) + input(type="range" id=slider_id disabled=!enabled) td.value-number(id=value_id) From 430a3848f7e44bd481eecdf511c06f3bbeb67031 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 31 Jan 2025 00:25:59 +0200 Subject: [PATCH 006/210] web: commented invalid css --- web/share/css/navbar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/share/css/navbar.css b/web/share/css/navbar.css index f3e7c0cc..ab85fe49 100644 --- a/web/share/css/navbar.css +++ b/web/share/css/navbar.css @@ -133,7 +133,7 @@ ul#navbar li div.menu::-webkit-scrollbar-thumb { } @-moz-document url-prefix() { ul#navbar li div.menu { - scrollbar-width: 8px; + /* scrollbar-width: 8px; px is not supported */ scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg); } } From 9436bb029df236cfe8d7fb7469aaac9b9cda49ab Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 1 Feb 2025 08:16:25 +0200 Subject: [PATCH 007/210] web: removed gop link --- web/kvm/index.html | 2 +- web/kvm/navbar-system.pug | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/web/kvm/index.html b/web/kvm/index.html index c45ed827..f06140d3 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -224,7 +224,7 @@
    H.264 gop: + H.264 gop: diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index a750e0cf..336867db 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -47,8 +47,7 @@ li.right#system-dropdown tr.feature-disabled#stream-h264-bitrate +menu_slider_td3("stream-h264-bitrate-slider", "stream-h264-bitrate-value", false) H.264 kbps: tr.feature-disabled#stream-h264-gop - +menu_slider_td3("stream-h264-gop-slider", "stream-h264-gop-value", false) - |H.264 #[a(target="_blank" href="https://docs.pikvm.org/webrtc") gop]: + +menu_slider_td3("stream-h264-gop-slider", "stream-h264-gop-value", false) H.264 gop: tr.feature-disabled#stream-mode +menu_radio_td2("stream-mode-radio", [ {title: "WebRTC
    H.264", value: "janus"}, From 13fff8a88c5fdd050d36ce5df1c5ee395c7121ba Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 1 Feb 2025 08:29:36 +0200 Subject: [PATCH 008/210] web: preparing to relative paths --- web/base.pug | 29 +++++++++++++++++------------ web/index.pug | 2 +- web/kvm/index.pug | 8 ++++++-- web/login/index.pug | 2 +- web/share/site.webmanifest | 2 +- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/web/base.pug b/web/base.pug index 004c849f..5d7de14f 100644 --- a/web/base.pug +++ b/web/base.pug @@ -23,36 +23,41 @@ doctype html ============================================================================== - - var css_dir = "/share/css" - var js_dir = "/share/js" - var svg_dir = "/share/svg" - var png_dir = "/share/png" + var prefix = "/" title = "" main_js = "" body_class = "" - css_list = ["vars", "main"] + css_list = [] block vars +block _vars_dynamic + - + share_dir = `${prefix}share` + css_dir = `${share_dir}/css` + js_dir = `${share_dir}/js` + svg_dir = `${share_dir}/svg` + png_dir = `${share_dir}/png` + + html(lang="en") head meta(charset="utf-8") title #{title} - link(rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png") - link(rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png") - link(rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png") - link(rel="manifest" href="/share/site.webmanifest") - link(rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5") + link(rel="apple-touch-icon" sizes="180x180" href=`${share_dir}/apple-touch-icon.png`) + link(rel="icon" type="image/png" sizes="32x32" href=`${share_dir}/favicon-32x32.png`) + link(rel="icon" type="image/png" sizes="16x16" href=`${share_dir}/favicon-16x16.png`) + link(rel="manifest" href=`${share_dir}/site.webmanifest`) + link(rel="mask-icon" href=`${share_dir}/safari-pinned-tab.svg` color="#5bbad5") meta(name="msapplication-TileColor" content="#2b5797") meta(name="theme-color" content="#ffffff") - each name in css_list + each name in ["vars", "main"].concat(css_list).concat(["user"]) link(rel="stylesheet" href=`${css_dir}/${name}.css`) - link(rel="stylesheet" href=`${css_dir}/user.css`) if main_js script(type="module") diff --git a/web/index.pug b/web/index.pug index 29de6da7..4543cf6c 100644 --- a/web/index.pug +++ b/web/index.pug @@ -5,7 +5,7 @@ append vars - title = "PiKVM Index" main_js = "index/main" - css_list = css_list.concat(["window", "modal", "index/index"]) + css_list.push("window", "modal", "index/index") block start diff --git a/web/kvm/index.pug b/web/kvm/index.pug index 6bf69430..fe04e570 100644 --- a/web/kvm/index.pug +++ b/web/kvm/index.pug @@ -6,8 +6,12 @@ append vars title = "PiKVM Session" main_js = "kvm/main" body_class = "body-no-select" - css_list = css_list.concat(["navbar", "window", "modal", "led", "slider", "switch", "radio", "progress", "keypad", "tabs"]) - css_list = css_list.concat(["kvm/stream", "kvm/hid", "kvm/msd", "kvm/system", "kvm/keyboard", "kvm/about"]) + css_list.push( + "navbar", "window", "modal", "led", "slider", + "switch", "radio", "progress", "keypad", "tabs", + "kvm/stream", "kvm/hid", "kvm/msd", + "kvm/system", "kvm/keyboard", "kvm/about" + ) block body diff --git a/web/login/index.pug b/web/login/index.pug index 99097a27..69681255 100644 --- a/web/login/index.pug +++ b/web/login/index.pug @@ -5,7 +5,7 @@ append vars - title = "PiKVM Login" main_js = "login/main" - css_list = css_list.concat(["window", "modal", "login/login"]) + css_list.push("window", "modal", "login/login") block body diff --git a/web/share/site.webmanifest b/web/share/site.webmanifest index a1f95a9a..d939d130 100644 --- a/web/share/site.webmanifest +++ b/web/share/site.webmanifest @@ -4,7 +4,7 @@ "start_url": "/", "icons": [ { - "src": "/share/android-chrome-192x192.png", + "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" } From b51ea5e37475f1bbff43e728ddb55f55a921471f Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 1 Feb 2025 08:58:04 +0200 Subject: [PATCH 009/210] web: relative html --- web/base.pug | 2 +- web/index.html | 28 +++++------ web/ipmi/index.html | 20 ++++---- web/ipmi/index.pug | 1 + web/kvm/index.html | 116 +++++++++++++++++++++---------------------- web/kvm/index.pug | 1 + web/login/index.html | 24 ++++----- web/login/index.pug | 1 + web/vnc/index.html | 20 ++++---- web/vnc/index.pug | 1 + 10 files changed, 109 insertions(+), 105 deletions(-) diff --git a/web/base.pug b/web/base.pug index 5d7de14f..4dd5f04a 100644 --- a/web/base.pug +++ b/web/base.pug @@ -23,7 +23,7 @@ doctype html ============================================================================== - - var prefix = "/" + var prefix = "./" title = "" main_js = "" diff --git a/web/index.html b/web/index.html index b92ee5f2..a1ad9c54 100644 --- a/web/index.html +++ b/web/index.html @@ -26,21 +26,21 @@ PiKVM Index - - - - - + + + + + - - - - - - - - @@ -49,7 +49,7 @@
    - + @@ -385,14 +386,14 @@ export function Switch() { " : ""} From c8df6211722dab107307bcafd7e870b33ab37e9f Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 2 Feb 2025 07:16:21 +0200 Subject: [PATCH 013/210] =?UTF-8?q?Bump=20version:=204.50=20=E2=86=92=204.?= =?UTF-8?q?51?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 83d84254..094032c0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.50 +current_version = 4.51 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 4ac0dda6..315ff6cf 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.50 +pkgver=4.51 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index e81b511d..ede300c5 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.50" +__version__ = "4.51" diff --git a/setup.py b/setup.py index f4dbf83f..3dbfb693 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.50", + version="4.51", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 1c179da857006852ec037109f3e7f9d51c17022d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 3 Feb 2025 09:51:25 +0200 Subject: [PATCH 014/210] web: orientation changing for media --- web/share/js/kvm/stream.js | 15 +++++--- web/share/js/kvm/stream_media.js | 63 +++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index 34baaa92..14622c5b 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -72,10 +72,10 @@ export function Streamer() { tools.radio.setOnClick("stream-mode-radio", __clickModeRadio, false); // Not getInt() because of radio is a string container. - // Also don't reset Janus at class init. + // Also don't reset Streamer at class init. tools.radio.clickValue("stream-orient-radio", tools.storage.get("stream.orient", 0)); tools.radio.setOnClick("stream-orient-radio", function() { - if (__streamer.getMode() === "janus") { // Right now it's working only for H.264 + if (["janus", "media"].includes(__streamer.getMode())) { let orient = parseInt(tools.radio.getValue("stream-orient-radio")); tools.storage.setInt("stream.orient", orient); if (__streamer.getOrientation() !== orient) { @@ -299,19 +299,22 @@ export function Streamer() { mode = __streamer.getMode(); } __streamer.stopStream(); + let orient = tools.storage.getInt("stream.orient", 0); if (mode === "janus") { - __streamer = new JanusStreamer(__setActive, __setInactive, __setInfo, - tools.storage.getInt("stream.orient", 0), !$("stream-video").muted, $("stream-mic-switch").checked); + let allow_audio = !$("stream-video").muted; + let allow_mic = $("stream-mic-switch").checked; + __streamer = new JanusStreamer(__setActive, __setInactive, __setInfo, orient, allow_audio, allow_mic); // Firefox doesn't support RTP orientation: // - https://bugzilla.mozilla.org/show_bug.cgi?id=1316448 tools.feature.setEnabled($("stream-orient"), !tools.browser.is_firefox); } else { if (mode === "media") { - __streamer = new MediaStreamer(__setActive, __setInactive, __setInfo); + __streamer = new MediaStreamer(__setActive, __setInactive, __setInfo, orient); + tools.feature.setEnabled($("stream-orient"), true); } else { // mjpeg __streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo); + tools.feature.setEnabled($("stream-orient"), false); } - tools.feature.setEnabled($("stream-orient"), false); tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js tools.feature.setEnabled($("stream-mic"), false); // Ditto } diff --git a/web/share/js/kvm/stream_media.js b/web/share/js/kvm/stream_media.js index ae751f9d..0e552677 100644 --- a/web/share/js/kvm/stream_media.js +++ b/web/share/js/kvm/stream_media.js @@ -26,7 +26,7 @@ import {tools, $} from "../tools.js"; -export function MediaStreamer(__setActive, __setInactive, __setInfo) { +export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { var self = this; /************************************************************************/ @@ -43,11 +43,12 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) { var __ctx = __canvas.getContext("2d"); var __state = null; - var __frames = 0; + var __fps_accum = 0; /************************************************************************/ - self.getName = () => "HTTP H.264"; + self.getOrientation = () => __orient; + self.getName = () => "Direct H.264"; self.getMode = () => "media"; self.getResolution = function() { @@ -110,8 +111,8 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) { if (__decoder && __decoder.state === "configured") { let online = !!(__state && __state.source.online); - let info = `${__frames} fps dynamic`; - __frames = 0; + let info = `${__fps_accum} fps dynamic`; + __fps_accum = 0; __setInfo(true, online, info); } } catch (ex) { @@ -145,7 +146,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) { __decoder = null; } __missed_heartbeats = 0; - __frames = 0; + __fps_accum = 0; __ws = null; if (!__stop) { setTimeout(() => __ensureMedia(true), 1000); @@ -203,18 +204,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) { } __decoder = new VideoDecoder({ // eslint-disable-line no-undef - "output": (frame) => { - try { - if (__canvas.width !== frame.displayWidth || __canvas.height !== frame.displayHeight) { - __canvas.width = frame.displayWidth; - __canvas.height = frame.displayHeight; - } - __ctx.drawImage(frame, 0, 0); - __frames += 1; - } finally { - frame.close(); - } - }, + "output": __drawFrame, "error": (err) => __logInfo(err.message), }); __codec = `avc1.${formats.h264.profile_level_id}`; @@ -225,6 +215,43 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) { })); }; + var __drawFrame = function(frame) { + try { + let width = frame.displayWidth; + let height = frame.displayHeight; + switch (__orient) { + case 90: case 270: + width = frame.displayHeight; + height = frame.displayWidth; + } + + if (__canvas.width !== width || __canvas.height !== height) { + __canvas.width = width; + __canvas.height = height; + } + + if (__orient === 0) { + __ctx.drawImage(frame, 0, 0); + } else { + __ctx.save(); + try { + switch(__orient) { + case 90: __ctx.translate(0, height); __ctx.rotate(-Math.PI / 2); break; + case 180: __ctx.translate(width, height); __ctx.rotate(-Math.PI); break; + case 270: __ctx.translate(width, 0); __ctx.rotate(Math.PI / 2); break; + } + __ctx.drawImage(frame, 0, 0); + } finally { + __ctx.restore(); + } + } + + __fps_accum += 1; + } finally { + frame.close(); + } + }; + var __decoderDestroy = function() { if (__decoder !== null) { __decoder.close(); From beb5d541b03be044f03125578d5a1b77064c197b Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 3 Feb 2025 09:52:21 +0200 Subject: [PATCH 015/210] =?UTF-8?q?Bump=20version:=204.51=20=E2=86=92=204.?= =?UTF-8?q?52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 094032c0..54cc5380 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.51 +current_version = 4.52 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 315ff6cf..06e829ce 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.51 +pkgver=4.52 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index ede300c5..7f303515 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.51" +__version__ = "4.52" diff --git a/setup.py b/setup.py index 3dbfb693..1410722f 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.51", + version="4.52", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 94fe2226f1b55c5f8fdb12d5b502c88dea78e989 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 6 Feb 2025 14:09:58 +0200 Subject: [PATCH 016/210] js cleanup --- web/share/js/index/main.js | 140 ++++++----- web/share/js/ipmi/main.js | 60 +++-- web/share/js/kvm/atx.js | 2 +- web/share/js/kvm/gpio.js | 39 +-- web/share/js/kvm/mouse.js | 6 +- web/share/js/kvm/msd.js | 21 +- web/share/js/kvm/stream_media.js | 3 +- web/share/js/kvm/stream_mjpeg.js | 6 +- web/share/js/kvm/switch.js | 8 +- web/share/js/login/main.js | 36 +-- web/share/js/tools.js | 43 ++-- web/share/js/vnc/main.js | 30 ++- web/share/js/wm.js | 408 ++++++++++++++++--------------- 13 files changed, 445 insertions(+), 357 deletions(-) diff --git a/web/share/js/index/main.js b/web/share/js/index/main.js index 9ebb1f60..aa425c00 100644 --- a/web/share/js/index/main.js +++ b/web/share/js/index/main.js @@ -39,84 +39,100 @@ export function main() { } function __setAppText() { + let e_href = tools.escape(window.location.href); $("app-text").innerHTML = ` # On Linux using Chromium/Chrome via any terminal:
    - $
    \`which chromium 2>/dev/null || which chrome 2>/dev/null || which google-chrome\` --app="${window.location.href}"
    + $ \`which chromium 2>/dev/null || which chrome 2>/dev/null || which google-chrome\` --app="${e_href}"

    # On MacOS using Terminal application:
    - $
    /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app="${window.location.href}"
    + $ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app="${e_href}"

    # On Windows via cmd.exe:
    - C:\>
    start chrome --app="${window.location.href}" + C:\> start chrome --app="${e_href}" `; } function __loadKvmdInfo() { tools.httpGet("api/info", {"fields": "auth,meta,extras"}, function(http) { - if (http.status === 200) { - let info = JSON.parse(http.responseText).result; + switch (http.status) { + case 200: + __showKvmdInfo(JSON.parse(http.responseText).result); + break; - let apps = []; - if (info.extras === null) { - wm.error("Not all applications in the menu can be displayed due an error.
    See KVMD logs for details."); - } else { - apps = Object.values(info.extras).sort(function(a, b) { - if (a.place < b.place) { - return -1; - } else if (a.place > b.place) { - return 1; - } else { - return 0; - } - }); - } + case 401: + case 403: + tools.currentOpen("login"); + break; - $("apps-box").innerHTML = "
      "; - - // Don't use this option, it may be removed in any time - let hide_kvm_button = ( - (info.meta !== null && info.meta.web && info.meta.web.hide_kvm_button) - || tools.config.getBool("index--hide-kvm-button", false) - ); - if (!hide_kvm_button) { - $("apps").innerHTML += __makeApp(null, "kvm", "share/svg/kvm.svg", "KVM"); - } - - for (let app of apps) { - if (app.place >= 0 && (app.enabled || app.started)) { - $("apps").innerHTML += __makeApp(null, app.path, app.icon, app.name); - } - } - - if (info.auth.enabled) { - $("apps").innerHTML += __makeApp("logout-button", "#", "share/svg/logout.svg", "Logout"); - tools.el.setOnClick($("logout-button"), __logout); - } - - if (info.meta !== null && info.meta.server && info.meta.server.host) { - $("kvmd-meta-server-host").innerHTML = info.meta.server.host; - document.title = `PiKVM Index: ${info.meta.server.host}`; - } else { - $("kvmd-meta-server-host").innerHTML = ""; - document.title = "PiKVM Index"; - } - } else if (http.status === 401 || http.status === 403) { - tools.currentOpen("login"); - } else { - setTimeout(__loadKvmdInfo, 1000); + default: + setTimeout(__loadKvmdInfo, 1000); + break; } }); } +function __showKvmdInfo(info) { + let apps = []; + if (info.extras === null) { + wm.error("Not all applications in the menu can be displayed due an error.
      See KVMD logs for details."); + } else { + apps = Object.values(info.extras).sort(function(a, b) { + if (a.place < b.place) { + return -1; + } else if (a.place > b.place) { + return 1; + } else { + return 0; + } + }); + } + + let html = ""; + + // Don't use this option, it may be removed in any time + let hide_kvm_button = ( + (info.meta !== null && info.meta.web && info.meta.web.hide_kvm_button) + || tools.config.getBool("index--hide-kvm-button", false) + ); + if (!hide_kvm_button) { + html += __makeApp(null, "kvm", "share/svg/kvm.svg", "KVM"); + } + + for (let app of apps) { + if (app.place >= 0 && (app.enabled || app.started)) { + html += __makeApp(null, app.path, app.icon, app.name); + } + } + + if (info.auth.enabled) { + html += __makeApp("logout-button", "#", "share/svg/logout.svg", "Logout"); + } + + $("apps-box").innerHTML = `
        ${html}
      `; + + if (info.auth.enabled) { + tools.el.setOnClick($("logout-button"), __logout); + } + + if (info.meta !== null && info.meta.server && info.meta.server.host) { + $("kvmd-meta-server-host").innerHTML = info.meta.server.host; + document.title = `PiKVM Index: ${info.meta.server.host}`; + } else { + $("kvmd-meta-server-host").innerHTML = ""; + document.title = "PiKVM Index"; + } +} + function __makeApp(id, path, icon, name) { // Tailing slash in href is added to avoid Nginx 301 redirect // when the location doesn't have tailing slash: "foo -> foo/". // Reverse proxy over PiKVM can be misconfigured to handle this. + let e_add_id = (id ? `id="${tools.escape(id)}"` : ""); return `
    • -
      - +
      +
      - + ${tools.escape(name)}
      @@ -126,10 +142,16 @@ function __makeApp(id, path, icon, name) { function __logout() { tools.httpPost("api/auth/logout", null, function(http) { - if (http.status === 200 || http.status === 401 || http.status === 403) { - tools.currentOpen("login"); - } else { - wm.error("Logout error", http.responseText); + switch (http.status) { + case 200: + case 401: + case 403: + tools.currentOpen("login"); + break; + + default: + wm.error("Logout error", http.responseText); + break; } }); } diff --git a/web/share/js/ipmi/main.js b/web/share/js/ipmi/main.js index 3f08e315..570cb839 100644 --- a/web/share/js/ipmi/main.js +++ b/web/share/js/ipmi/main.js @@ -32,29 +32,43 @@ export function main() { function __loadKvmdInfo() { tools.httpGet("api/info", null, function(http) { - if (http.status === 200) { - let ipmi_port = JSON.parse(http.responseText).result.extras.ipmi.port; - let make_item = (comment, ipmi, api) => ` - # ${comment}:
      $
      - ipmitool -I lanplus -U admin -P admin -H ${window.location.hostname} -p ${ipmi_port} ${ipmi}
      - $ curl -XPOST -HX-KVMD-User:admin -HX-KVMD-Passwd:admin -k \\
      -     ${window.location.protocol}//${window.location.host}/api/atx${api}
      - `; - $("ipmi-text").innerHTML = ` - ${make_item("Power on the server if it's off", "power on", "/power?action=on")} -
      - ${make_item("Soft power off the server if it's on", "power soft", "/power?action=off")} -
      - ${make_item("Hard power off the server if it's on", "power off", "/power?action=off_hard")} -
      - ${make_item("Hard reset the server if it's on", "power reset", "/power?action=reset_hard")} -
      - ${make_item("Check the power status", "power status", "")} - `; - } else if (http.status === 401 || http.status === 403) { - tools.currentOpen("login"); - } else { - setTimeout(__loadKvmdInfo, 1000); + switch (http.status) { + case 200: + __showKvmdInfo(JSON.parse(http.responseText).result); + break; + + case 401: + case 403: + tools.currentOpen("login"); + break; + + default: + setTimeout(__loadKvmdInfo, 1000); + break; } }); } + +function __showKvmdInfo(info) { + let make_item = function (comment, cmd, api) { + return ` + + # ${tools.escape(comment)}:
      $ +
      + ipmitool -I lanplus -U admin -P admin + -H ${tools.escape(window.location.hostname)} + -p ${tools.escape(info.extras.ipmi.port)} ${tools.escape(cmd)} +
      + $ + curl -XPOST -HX-KVMD-User:admin -HX-KVMD-Passwd:admin -k \\
           + ${tools.escape(window.location.protocol + "//" + window.location.host + "/api/atx" + api)} + `; + }; + $("ipmi-text").innerHTML = [ + make_item("Power on the server if it's off", "power on", "/power?action=on"), + make_item("Soft power off the server if it's on", "power soft", "/power?action=off"), + make_item("Hard power off the server if it's on", "power off", "/power?action=off_hard"), + make_item("Hard reset the server if it's on", "power reset", "/power?action=reset_hard"), + make_item("Check the power status", "power status", ""), + ].join("

      "); +} diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js index 6bdcf571..69a0d2c5 100644 --- a/web/share/js/kvm/atx.js +++ b/web/share/js/kvm/atx.js @@ -106,7 +106,7 @@ export function Atx(__recorder) { if ($("atx-ask-switch").checked) { wm.confirm(` - Are you sure you want to press the ${button} button?
      + Are you sure you want to press the ${tools.escape(button)} button?
      Warning! This could cause data loss on the server. `).then(function(ok) { if (ok) { diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index d4221b3d..fd602ce8 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -135,30 +135,36 @@ export function Gpio(__recorder) { var __createItem = function(item) { if (item.type === "label") { return item.text; + } else if (item.type === "input") { + let e_ch_class = tools.escape(`__gpio-led-${item.channel}`); + let e_icon = tools.escape(`${ROOT_PREFIX}share/svg/led-circle.svg`); return ` `; + } else if (item.type === "output") { let controls = []; - let confirm = (item.confirm ? "Are you sure you want to perform this action?" : ""); + let e_ch = tools.escape(item.channel); + let e_confirm = (item.confirm ? tools.escape("Are you sure you want to perform this action?") : ""); if (item.scheme["switch"]) { - let id = tools.makeId(); + let e_id = tools.escape(`__gpio-switch-${tools.makeRandomId()}`); + let e_ch_class = tools.escape(`__gpio-switch-${item.channel}`); controls.push(`
    • `); } return `
      diff --git a/web/ipmi/index.html b/web/ipmi/index.html index 5e50f4ce..ca68ab56 100644 --- a/web/ipmi/index.html +++ b/web/ipmi/index.html @@ -26,18 +26,18 @@ PiKVM IPMI Info - - - - - + + + + + - - - - - diff --git a/web/ipmi/index.pug b/web/ipmi/index.pug index c7729dfe..6fb6bb68 100644 --- a/web/ipmi/index.pug +++ b/web/ipmi/index.pug @@ -3,6 +3,7 @@ extends ../start.pug append vars - + prefix = "../" title = "PiKVM IPMI Info" main_js = "ipmi/main" index_link = true diff --git a/web/kvm/index.html b/web/kvm/index.html index f06140d3..41529564 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -26,46 +26,46 @@ PiKVM Session - - - - - + + + + + - - - - - - - - - - - - - - - - - - - -
      - + @@ -79,7 +79,7 @@
      Raspberry Pi's health is at risk
      - + @@ -94,7 +94,7 @@
      Undervoltage detected
      - + @@ -108,12 +108,12 @@
      Overheating detected
      - + @@ -127,7 +127,7 @@
      Raspberry Pi's health is at risk
      - + @@ -139,7 +139,7 @@ -
    • System +
    • System
    • Fan failed
      @@ -163,7 +163,7 @@
      - +
      WebRTC is not supported by this browser
      @@ -174,7 +174,7 @@
      - +
      Direct H.264 streaming is not supported
      @@ -185,7 +185,7 @@
      - +
      H.264 is not supported by this browser
      @@ -497,7 +497,7 @@
      -
    • ATX +
    • ATX
    • -
    • Drive +
    • Drive
    • -
    • Macro +
    • Macro
    • -
    • Text +
    • Text
    • GPIO
    • -
    • Switch +
    • Switch @@ -2283,7 +2285,9 @@
      -
      // You can get this JSON using handle /api/info?fields=meta
      +
      + // You can get this JSON using handle + /api/info?fields=meta
      // In the standard configuration this data
      // is specified in the file /etc/kvmd/meta.yaml

      No data
      diff --git a/web/kvm/index.pug b/web/kvm/index.pug index 5b0bf260..d202ada4 100644 --- a/web/kvm/index.pug +++ b/web/kvm/index.pug @@ -3,7 +3,7 @@ extends ../base.pug append vars - - prefix = "../" + root_prefix = "../" title = "PiKVM Session" main_js = "kvm/main" body_class = "body-no-select" diff --git a/web/kvm/navbar-health.pug b/web/kvm/navbar-health.pug index f33e3482..91696915 100644 --- a/web/kvm/navbar-health.pug +++ b/web/kvm/navbar-health.pug @@ -31,4 +31,4 @@ #fan-health-message-fail hr +menu_message("led-fan", "Fan failed", "led-gray") - | A fan error occured, please #[a(href="/api/log?seek=3600&follow=1" target="_blank") check the log] + | A fan error occured, please #[a(href=`${root_prefix}api/log?seek=3600&follow=1` target="_blank") check the log] diff --git a/web/kvm/navbar.pug b/web/kvm/navbar.pug index 4ea9812a..9ef1464e 100644 --- a/web/kvm/navbar.pug +++ b/web/kvm/navbar.pug @@ -83,7 +83,7 @@ mixin menu_spoiler(title) ul#navbar li.left - a(id="logo" href="/") ←   + a(id="logo" href=root_prefix) ←   img.svg-gray(src=`${svg_dir}/logo.svg` alt="π-kvm") include navbar-health.pug diff --git a/web/kvm/window-about.pug b/web/kvm/window-about.pug index 0c717aa3..8a1dc361 100644 --- a/web/kvm/window-about.pug +++ b/web/kvm/window-about.pug @@ -34,7 +34,8 @@ mixin about_tab(name, title, checked=false) +about_tab("meta", "Meta", true) div span.code-comment - | // You can get this JSON using handle #[a(target="_blank" href="/api/info?fields=meta") /api/info?fields=meta]#[br] + | // You can get this JSON using handle + | #[a(target="_blank" href=`${root_prefix}api/info?fields=meta`) /api/info?fields=meta]#[br] | // In the standard configuration this data#[br] | // is specified in the file /etc/kvmd/meta.yaml br diff --git a/web/login/index.html b/web/login/index.html index 389b5871..a8cbedd9 100644 --- a/web/login/index.html +++ b/web/login/index.html @@ -39,7 +39,9 @@ - diff --git a/web/login/index.pug b/web/login/index.pug index 084e017c..11600c01 100644 --- a/web/login/index.pug +++ b/web/login/index.pug @@ -3,7 +3,7 @@ extends ../base.pug append vars - - prefix = "../" + root_prefix = "../" title = "PiKVM Login" main_js = "login/main" css_list.push("window", "modal", "login/login") diff --git a/web/share/js/bb.js b/web/share/js/bb.js index ba49efa6..38549da6 100644 --- a/web/share/js/bb.js +++ b/web/share/js/bb.js @@ -23,6 +23,9 @@ "use strict"; +import {ROOT_PREFIX} from "./vars.js"; + + export var browser = new function() { // https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser/9851769 // https://github.com/fingerprintjs/fingerprintjs/discussions/641 @@ -133,12 +136,12 @@ export function checkBrowser(desktop_css, mobile_css) { let force_desktop = (new URL(window.location.href)).searchParams.get("force_desktop"); let force_mobile = (new URL(window.location.href)).searchParams.get("force_mobile"); if ((force_desktop || !browser.is_mobile) && !force_mobile) { - __addCssLink("/share/css/x-desktop.css"); + __addCssLink("x-desktop.css"); if (desktop_css) { __addCssLink(desktop_css); } } else { - __addCssLink("/share/css/x-mobile.css"); + __addCssLink("x-mobile.css"); if (mobile_css) { __addCssLink(mobile_css); } @@ -148,6 +151,7 @@ export function checkBrowser(desktop_css, mobile_css) { } function __addCssLink(path) { + path = `${ROOT_PREFIX}share/css/${path}`; console.log("===== Adding CSS:", path); let el_head = document.getElementsByTagName("head")[0]; let el_link = document.createElement("link"); diff --git a/web/share/js/index/main.js b/web/share/js/index/main.js index 19cba440..9ebb1f60 100644 --- a/web/share/js/index/main.js +++ b/web/share/js/index/main.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "../vars.js"; import {tools, $} from "../tools.js"; import {checkBrowser} from "../bb.js"; import {wm, initWindowManager} from "../wm.js"; @@ -51,7 +52,7 @@ function __setAppText() { } function __loadKvmdInfo() { - tools.httpGet("/api/info", {"fields": "auth,meta,extras"}, function(http) { + tools.httpGet("api/info", {"fields": "auth,meta,extras"}, function(http) { if (http.status === 200) { let info = JSON.parse(http.responseText).result; @@ -100,7 +101,7 @@ function __loadKvmdInfo() { document.title = "PiKVM Index"; } } else if (http.status === 401 || http.status === 403) { - document.location.href = "/login"; + tools.currentOpen("login"); } else { setTimeout(__loadKvmdInfo, 1000); } @@ -108,11 +109,14 @@ function __loadKvmdInfo() { } function __makeApp(id, path, icon, name) { + // Tailing slash in href is added to avoid Nginx 301 redirect + // when the location doesn't have tailing slash: "foo -> foo/". + // Reverse proxy over PiKVM can be misconfigured to handle this. return `
    • - +
      - + ${tools.escape(name)}
      @@ -121,9 +125,9 @@ function __makeApp(id, path, icon, name) { } function __logout() { - tools.httpPost("/api/auth/logout", null, function(http) { + tools.httpPost("api/auth/logout", null, function(http) { if (http.status === 200 || http.status === 401 || http.status === 403) { - document.location.href = "/login"; + tools.currentOpen("login"); } else { wm.error("Logout error", http.responseText); } diff --git a/web/share/js/ipmi/main.js b/web/share/js/ipmi/main.js index 26bd1f38..3f08e315 100644 --- a/web/share/js/ipmi/main.js +++ b/web/share/js/ipmi/main.js @@ -31,7 +31,7 @@ export function main() { } function __loadKvmdInfo() { - tools.httpGet("/api/info", null, function(http) { + tools.httpGet("api/info", null, function(http) { if (http.status === 200) { let ipmi_port = JSON.parse(http.responseText).result.extras.ipmi.port; let make_item = (comment, ipmi, api) => ` @@ -52,7 +52,7 @@ function __loadKvmdInfo() { ${make_item("Check the power status", "power status", "")} `; } else if (http.status === 401 || http.status === 403) { - document.location.href = "/login"; + tools.currentOpen("login"); } else { setTimeout(__loadKvmdInfo, 1000); } diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js index ce3e4d27..6bdcf571 100644 --- a/web/share/js/kvm/atx.js +++ b/web/share/js/kvm/atx.js @@ -94,7 +94,7 @@ export function Atx(__recorder) { var __clickAtx = function(button) { let click_button = function() { - tools.httpPost("/api/atx/click", {"button": button}, function(http) { + tools.httpPost("api/atx/click", {"button": button}, function(http) { if (http.status === 409) { wm.error("Performing another ATX operation for other client.
      Please try again later."); } else if (http.status !== 200) { diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index 41d5ee92..d4221b3d 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "../vars.js"; import {tools, $, $$} from "../tools.js"; import {wm} from "../wm.js"; @@ -138,7 +139,7 @@ export function Gpio(__recorder) { return ` `; @@ -202,7 +203,7 @@ export function Gpio(__recorder) { confirm = el.getAttribute("data-confirm-off"); } let act = () => { - __sendPost("/api/gpio/switch", {"channel": ch, "state": to}); + __sendPost("api/gpio/switch", {"channel": ch, "state": to}); __recorder.recordGpioSwitchEvent(ch, to); }; if (confirm) { @@ -220,7 +221,7 @@ export function Gpio(__recorder) { let ch = el.getAttribute("data-channel"); let confirm = el.getAttribute("data-confirm"); let act = () => { - __sendPost("/api/gpio/pulse", {"channel": ch}); + __sendPost("api/gpio/pulse", {"channel": ch}); __recorder.recordGpioPulseEvent(ch); }; if (confirm) { diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index 65bd480d..e3466896 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -275,7 +275,7 @@ export function Hid(__getGeometry, __recorder) { var __clickOutputsRadio = function(hid) { let output = tools.radio.getValue(`hid-outputs-${hid}-radio`); - tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) { + tools.httpPost("api/hid/set_params", {[`${hid}_output`]: output}, function(http) { if (http.status !== 200) { wm.error("Can't configure HID", http.responseText); } @@ -284,7 +284,7 @@ export function Hid(__getGeometry, __recorder) { var __clickJigglerSwitch = function() { let enabled = $("hid-jiggler-switch").checked; - tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) { + tools.httpPost("api/hid/set_params", {"jiggler": enabled}, function(http) { if (http.status !== 200) { wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse jiggler`, http.responseText); } @@ -293,7 +293,7 @@ export function Hid(__getGeometry, __recorder) { var __clickConnectSwitch = function() { let connected = $("hid-connect-switch").checked; - tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) { + tools.httpPost("api/hid/set_connected", {"connected": connected}, function(http) { if (http.status !== 200) { wm.error(`Can't ${connected ? "connect" : "disconnect"} HID`, http.responseText); } @@ -303,7 +303,7 @@ export function Hid(__getGeometry, __recorder) { var __clickResetButton = function() { wm.confirm("Are you sure you want to reset HID (keyboard & mouse)?").then(function(ok) { if (ok) { - tools.httpPost("/api/hid/reset", null, function(http) { + tools.httpPost("api/hid/reset", null, function(http) { if (http.status !== 200) { wm.error("HID reset error", http.responseText); } diff --git a/web/share/js/kvm/main.js b/web/share/js/kvm/main.js index c71e7fd6..b62d4286 100644 --- a/web/share/js/kvm/main.js +++ b/web/share/js/kvm/main.js @@ -31,7 +31,7 @@ import {Session} from "./session.js"; export function main() { - if (checkBrowser(null, "/share/css/kvm/x-mobile.css")) { + if (checkBrowser(null, "kvm/x-mobile.css")) { tools.storage.bindSimpleSwitch($("page-close-ask-switch"), "page.close.ask", true, function(value) { if (value) { window.onbeforeunload = function(event) { @@ -48,7 +48,7 @@ export function main() { initWindowManager(); - tools.el.setOnClick($("open-log-button"), () => window.open("/api/log?seek=3600&follow=1", "_blank")); + tools.el.setOnClick($("open-log-button"), () => tools.windowOpen("api/log?seek=3600&follow=1")); tools.storage.bindSimpleSwitch( $("page-full-tab-stream-switch"), diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index f0192745..13d53079 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "../vars.js"; import {tools, $} from "../tools.js"; import {wm} from "../wm.js"; @@ -270,14 +271,14 @@ export function Msd() { var __clickDownloadButton = function() { let image = encodeURIComponent($("msd-image-selector").value); - window.open(`/api/msd/read?image=${image}`); + tools.windowOpen(`api/msd/read?image=${image}`); }; var __clickRemoveButton = function() { let name = $("msd-image-selector").value; wm.confirm("Are you sure you want to remove this image?", name).then(function(ok) { if (ok) { - tools.httpPost("/api/msd/remove", {"image": name}, function(http) { + tools.httpPost("api/msd/remove", {"image": name}, function(http) { if (http.status !== 200) { wm.error("Can't remove image", http.responseText); } @@ -287,7 +288,7 @@ export function Msd() { }; var __sendParam = function(name, value) { - tools.httpPost("/api/msd/set_params", {[name]: value}, function(http) { + tools.httpPost("api/msd/set_params", {[name]: value}, function(http) { if (http.status !== 200) { wm.error("Can't configure Mass Storage", http.responseText); } @@ -301,10 +302,10 @@ export function Msd() { let prefix = encodeURIComponent($("msd-new-part-selector").value); if (file) { let image = encodeURIComponent(file.name); - __http.open("POST", `/api/msd/write?prefix=${prefix}&image=${image}&remove_incomplete=1`, true); + __http.open("POST", `${ROOT_PREFIX}api/msd/write?prefix=${prefix}&image=${image}&remove_incomplete=1`, true); } else { let url = encodeURIComponent($("msd-new-url").value); - __http.open("POST", `/api/msd/write_remote?prefix=${prefix}&url=${url}&remove_incomplete=1`, true); + __http.open("POST", `${ROOT_PREFIX}api/msd/write_remote?prefix=${prefix}&url=${url}&remove_incomplete=1`, true); } __http.upload.timeout = 7 * 24 * 3600; __http.onreadystatechange = __uploadStateChange; @@ -360,7 +361,7 @@ export function Msd() { }; var __clickConnectButton = function(connected) { - tools.httpPost("/api/msd/set_connected", {"connected": connected}, function(http) { + tools.httpPost("api/msd/set_connected", {"connected": connected}, function(http) { if (http.status !== 200) { wm.error("Can't switch Mass Storage", http.responseText); } @@ -373,7 +374,7 @@ export function Msd() { var __clickResetButton = function() { wm.confirm("Are you sure you want to reset Mass Storage?").then(function(ok) { if (ok) { - tools.httpPost("/api/msd/reset", null, function(http) { + tools.httpPost("api/msd/reset", null, function(http) { if (http.status !== 200) { wm.error("Mass Storage reset error", http.responseText); } diff --git a/web/share/js/kvm/ocr.js b/web/share/js/kvm/ocr.js index b490956b..00f6dd3c 100644 --- a/web/share/js/kvm/ocr.js +++ b/web/share/js/kvm/ocr.js @@ -186,7 +186,7 @@ export function Ocr(__getGeometry) { "ocr_right": __sel.right, "ocr_bottom": __sel.bottom, }; - tools.httpGet("/api/streamer/snapshot", params, function(http) { + tools.httpGet("api/streamer/snapshot", params, function(http) { if (http.status === 200) { wm.copyTextToClipboard(http.responseText); } else { diff --git a/web/share/js/kvm/paste.js b/web/share/js/kvm/paste.js index f42fa22c..2714c158 100644 --- a/web/share/js/kvm/paste.js +++ b/web/share/js/kvm/paste.js @@ -72,7 +72,7 @@ export function Paste(__recorder) { tools.debug(`HID: paste-as-keys ${keymap}: ${text}`); - tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap, "slow": slow}, function(http) { + tools.httpPost("api/hid/print", {"limit": 0, "keymap": keymap, "slow": slow}, function(http) { tools.el.setEnabled($("hid-pak-text"), true); tools.el.setEnabled($("hid-pak-button"), true); tools.el.setEnabled($("hid-pak-keymap-selector"), true); diff --git a/web/share/js/kvm/recorder.js b/web/share/js/kvm/recorder.js index cf792bc2..2091e9a4 100644 --- a/web/share/js/kvm/recorder.js +++ b/web/share/js/kvm/recorder.js @@ -293,7 +293,7 @@ export function Recorder() { if (event.event.slow !== undefined) { params["slow"] = event.event.slow; } - tools.httpPost("/api/hid/print", params, function(http) { + tools.httpPost("api/hid/print", params, function(http) { if (http.status === 413) { wm.error("Too many text for paste!"); __stopProcess(); @@ -307,7 +307,7 @@ export function Recorder() { return; } else if (event.event_type === "atx_button") { - tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) { + tools.httpPost("api/atx/click", {"button": event.event.button}, function(http) { if (http.status !== 200) { wm.error("ATX error", http.responseText); __stopProcess(); @@ -318,7 +318,7 @@ export function Recorder() { return; } else if (["gpio_switch", "gpio_pulse"].includes(event.event_type)) { - let path = "/api/gpio"; + let path = "api/gpio"; let params = {"channel": event.event.channel}; if (event.event_type === "gpio_switch") { path += "/switch"; diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index fcf20ded..9e944e81 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "../vars.js"; import {tools, $} from "../tools.js"; import {wm} from "../wm.js"; @@ -272,10 +273,15 @@ export function Session() { let close_hook = null; let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started)); if (has_webterm) { - let path = "/" + state.webterm.path + "?disableLeaveAlert=true"; + let loc = window.location; + let base = `${loc.protocol}//${loc.host}${loc.pathname}${ROOT_PREFIX}`; + // Tailing slash after state.webterm.path is added to avoid Nginx 301 redirect + // when the location doesn't have tailing slash: "foo -> foo/". + // Reverse proxy over PiKVM can be misconfigured to handle this. + let url = base + state.webterm.path + "/?disableLeaveAlert=true"; show_hook = function() { - tools.info("Terminal opened: ", path); - $("webterm-iframe").src = path; + tools.info("Terminal opened: ", url); + $("webterm-iframe").src = url; }; close_hook = function() { tools.info("Terminal closed"); @@ -291,9 +297,9 @@ export function Session() { $("link-led").className = "led-yellow"; $("link-led").title = "Connecting..."; - tools.httpGet("/api/auth/check", null, function(http) { + tools.httpGet("api/auth/check", null, function(http) { if (http.status === 200) { - __ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`); + __ws = new WebSocket(tools.makeWsUrl("api/ws")); __ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event); __ws.onopen = __wsOpenHandler; __ws.onmessage = __wsMessageHandler; @@ -302,7 +308,7 @@ export function Session() { } else if (http.status === 401 || http.status === 403) { window.onbeforeunload = () => null; wm.error("Unexpected logout occured, please login again").then(function() { - document.location.href = "/login"; + tools.currentOpen("login"); }); } else { __wsCloseHandler(null); diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index 2de3d469..9df3c4b6 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -332,19 +332,14 @@ export function Streamer() { }; var __clickScreenshotButton = function() { - let el = document.createElement("a"); - el.href = "/api/streamer/snapshot"; - el.target = "_blank"; - document.body.appendChild(el); - el.click(); - setTimeout(() => document.body.removeChild(el), 0); + tools.windowOpen("api/streamer/snapshot"); }; var __clickResetButton = function() { wm.confirm("Are you sure you want to reset stream?").then(function(ok) { if (ok) { __resetStream(); - tools.httpPost("/api/streamer/reset", null, function(http) { + tools.httpPost("api/streamer/reset", null, function(http) { if (http.status !== 200) { wm.error("Can't reset stream", http.responseText); } @@ -354,7 +349,7 @@ export function Streamer() { }; var __sendParam = function(name, value) { - tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) { + tools.httpPost("api/streamer/set_params", {[name]: value}, function(http) { if (http.status !== 200) { wm.error("Can't configure stream", http.responseText); } diff --git a/web/share/js/kvm/stream_janus.js b/web/share/js/kvm/stream_janus.js index 449450b7..3e9ea5dd 100644 --- a/web/share/js/kvm/stream_janus.js +++ b/web/share/js/kvm/stream_janus.js @@ -96,7 +96,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _ __setInfo(false, false, ""); __logInfo("Starting Janus ..."); __janus = new _Janus({ - "server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`, + "server": tools.makeWsUrl("janus/ws"), "ipv6": true, "destroyOnUnload": false, "success": __attachJanus, diff --git a/web/share/js/kvm/stream_media.js b/web/share/js/kvm/stream_media.js index 89df9c28..ae751f9d 100644 --- a/web/share/js/kvm/stream_media.js +++ b/web/share/js/kvm/stream_media.js @@ -79,7 +79,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) { __setInactive(); __setInfo(false, false, ""); __logInfo("Starting Media ..."); - __ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/media/ws`); + __ws = new WebSocket(tools.makeWsUrl("api/media/ws")); __ws.binaryType = "arraybuffer"; __ws.onopen = __wsOpenHandler; __ws.onerror = __wsErrorHandler; diff --git a/web/share/js/kvm/stream_mjpeg.js b/web/share/js/kvm/stream_mjpeg.js index 1fd1548e..a13e708e 100644 --- a/web/share/js/kvm/stream_mjpeg.js +++ b/web/share/js/kvm/stream_mjpeg.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "../vars.js"; import {tools, $} from "../tools.js"; @@ -72,7 +73,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) { self.stopStream = function() { self.ensureStream(null); - let blank = "/share/png/blank-stream.png"; + let blank = `${ROOT_PREFIX}share/png/blank-stream.png`; if (!String.prototype.endsWith.call($("stream-image").src, blank)) { $("stream-image").src = blank; } @@ -138,7 +139,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) { __setStreamInactive(); __stopChecking(); - let path = `/streamer/stream?key=${__key}`; + let path = `${ROOT_PREFIX}streamer/stream?key=${__key}`; if (tools.browser.is_safari || tools.browser.is_ios) { // uStreamer fix for WebKit __logInfo("Using dual_final_frames=1 to fix WebKit bugs"); diff --git a/web/share/js/kvm/switch.js b/web/share/js/kvm/switch.js index 8187a22c..1ed9adca 100644 --- a/web/share/js/kvm/switch.js +++ b/web/share/js/kvm/switch.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "../vars.js"; import {tools, $} from "../tools.js"; import {wm} from "../wm.js"; @@ -125,7 +126,7 @@ export function Switch() { + ":" + brightness.toString(16).padStart(2, "0") + ":" + color.blink_ms.toString(16).padStart(4, "0") ); - __sendPost("/api/switch/set_colors", {[role]: rgbx}, function() { + __sendPost("api/switch/set_colors", {[role]: rgbx}, function() { el_color.value = ( "#" + color.red.toString(16).padStart(2, "0") @@ -137,7 +138,7 @@ export function Switch() { }; var __clickSetDefaultColorButton = function(role) { - __sendPost("/api/switch/set_colors", {[role]: "default"}); + __sendPost("api/switch/set_colors", {[role]: "default"}); }; var __applyEdids = function(edids) { @@ -210,7 +211,7 @@ export function Switch() { if (ok) { let name = $("__switch-edid-new-name-input").value; let data = $("__switch-edid-new-data-text").value; - __sendPost("/api/switch/edids/create", {"name": name, "data": data}); + __sendPost("api/switch/edids/create", {"name": name, "data": data}); } }); }; @@ -222,7 +223,7 @@ export function Switch() { let html = "Are you sure to remove this EDID?
      Ports that used it will change it to the default."; wm.confirm(html, name).then(function(ok) { if (ok) { - __sendPost("/api/switch/edids/remove", {"id": edid_id}); + __sendPost("api/switch/edids/remove", {"id": edid_id}); } }); } @@ -344,11 +345,11 @@ export function Switch() {
    • @@ -365,10 +366,10 @@ export function Switch() {
      - - - - + + + +
      @@ -524,7 +525,7 @@ export function Switch() { for (let action of Object.keys(atx_actions)) { params[`atx_click_${action}_delay`] = tools.slider.getValue($(`__switch-port-atx-click-${action}-delay-slider`)); }; - __sendPost("/api/switch/set_port_params", params); + __sendPost("api/switch/set_port_params", params); } }); }; @@ -555,31 +556,31 @@ export function Switch() { Otherwise, it will break a current USB operation (OS installation, Live CD, or whatever). `); } else { - __sendPost("/api/switch/set_active", {"port": port}); + __sendPost("api/switch/set_active", {"port": port}); } }; var __switchUplinkBeacon = function(unit) { let state = false; try { state = !__state.beacons.uplinks[unit]; } catch {}; // eslint-disable-line no-empty - __sendPost("/api/switch/set_beacon", {"uplink": unit, "state": state}); + __sendPost("api/switch/set_beacon", {"uplink": unit, "state": state}); }; var __switchDownlinkBeacon = function(unit) { let state = false; try { state = !__state.beacons.downlinks[unit]; } catch {}; // eslint-disable-line no-empty - __sendPost("/api/switch/set_beacon", {"downlink": unit, "state": state}); + __sendPost("api/switch/set_beacon", {"downlink": unit, "state": state}); }; var __switchPortBeacon = function(port) { let state = false; try { state = !__state.beacons.ports[port]; } catch {}; // eslint-disable-line no-empty - __sendPost("/api/switch/set_beacon", {"port": port, "state": state}); + __sendPost("api/switch/set_beacon", {"port": port, "state": state}); }; var __atxClick = function(port, button) { let click_button = function() { - __sendPost("/api/switch/atx/click", {"port": port, "button": button}); + __sendPost("api/switch/atx/click", {"port": port, "button": button}); }; if ($("switch-atx-ask-switch").checked) { wm.confirm(` diff --git a/web/share/js/login/main.js b/web/share/js/login/main.js index c779a6d9..63fa0dec 100644 --- a/web/share/js/login/main.js +++ b/web/share/js/login/main.js @@ -51,9 +51,9 @@ function __login() { } else { let passwd = $("passwd-input").value + $("code-input").value; let body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`; - tools.httpPost("/api/auth/login", null, function(http) { + tools.httpPost("api/auth/login", null, function(http) { if (http.status === 200) { - document.location.href = "/"; + tools.currentOpen(""); } else if (http.status === 403) { wm.error("Invalid credentials").then(__tryAgain); } else { diff --git a/web/share/js/tools.js b/web/share/js/tools.js index f5ddae8b..11f08735 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -23,6 +23,7 @@ "use strict"; +import {ROOT_PREFIX} from "./vars.js"; import {browser} from "./bb.js"; @@ -39,7 +40,16 @@ export var tools = new function() { /************************************************************************/ + self.currentOpen = function(url) { + window.location.href = ROOT_PREFIX + url; + }; + + self.windowOpen = function(url) { + window.open(ROOT_PREFIX + url, "_blank"); + }; + self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) { + url = ROOT_PREFIX + url; if (params) { params = new URLSearchParams(params); if (params) { @@ -68,6 +78,11 @@ export var tools = new function() { self.httpRequest("POST", url, params, callback, body, content_type, timeout); }; + self.makeWsUrl = function(url) { + let proto = (self.is_https ? "wss://" : "ws://"); + return proto + window.location.host + window.location.pathname + ROOT_PREFIX + url; + }; + /************************************************************************/ self.escape = function(text) { @@ -383,7 +398,7 @@ export var tools = new function() { /************************************************************************/ - self.is_https = (location.protocol === "https:"); + self.is_https = (window.location.protocol === "https:"); self.cookies = new function() { return { diff --git a/web/share/js/vars.js b/web/share/js/vars.js new file mode 100644 index 00000000..e76794da --- /dev/null +++ b/web/share/js/vars.js @@ -0,0 +1,31 @@ +/***************************************************************************** +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +*****************************************************************************/ + + +"use strict"; + + +export var ROOT_PREFIX = "./"; + + +export function setRootPrefix(prefix) { + ROOT_PREFIX = prefix; +} diff --git a/web/share/js/vnc/main.js b/web/share/js/vnc/main.js index 64294e3b..352826b7 100644 --- a/web/share/js/vnc/main.js +++ b/web/share/js/vnc/main.js @@ -31,7 +31,7 @@ export function main() { } function __loadKvmdInfo() { - tools.httpGet("/api/info", null, function(http) { + tools.httpGet("api/info", null, function(http) { if (http.status === 200) { let vnc_port = JSON.parse(http.responseText).result.extras.vnc.port; $("vnc-text").innerHTML = ` @@ -39,7 +39,7 @@ function __loadKvmdInfo() { $ vncviewer ${window.location.hostname}::${vnc_port} `; } else if (http.status === 401 || http.status === 403) { - document.location.href = "/login"; + tools.currentOpen("login"); } else { setTimeout(__loadKvmdInfo, 1000); } diff --git a/web/start.pug b/web/start.pug index 80968427..f02651ef 100644 --- a/web/start.pug +++ b/web/start.pug @@ -8,7 +8,7 @@ block body div(class="start-box") div(class="start") if index_link - a(style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href="/") + a(style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href=root_prefix) |   ←   [ PiKVM Index ] hr block start diff --git a/web/vnc/index.html b/web/vnc/index.html index f5e0f7c0..bee0926a 100644 --- a/web/vnc/index.html +++ b/web/vnc/index.html @@ -37,13 +37,15 @@ -
      -
        ←   [ PiKVM Index ] +
        ←   [ PiKVM Index ]

      This PiKVM device has running kvmd-vnc daemon and provides VNC access to the server.

      WARNING! We strongly don't recommend you to use VNC in untrusted networks without diff --git a/web/vnc/index.pug b/web/vnc/index.pug index 5d9c1146..de1a9c01 100644 --- a/web/vnc/index.pug +++ b/web/vnc/index.pug @@ -3,7 +3,7 @@ extends ../start.pug append vars - - prefix = "../" + root_prefix = "../" title = "PiKVM VNC Info" main_js = "vnc/main" index_link = true From 4800f9e486129fc4112eb87f88ff8a335cc15a50 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 2 Feb 2025 07:10:11 +0200 Subject: [PATCH 011/210] nginx: removed legacy limit_rate --- configs/nginx/loc-bigpost.conf | 2 -- 1 file changed, 2 deletions(-) diff --git a/configs/nginx/loc-bigpost.conf b/configs/nginx/loc-bigpost.conf index ebd37a6b..7125ecc7 100644 --- a/configs/nginx/loc-bigpost.conf +++ b/configs/nginx/loc-bigpost.conf @@ -1,4 +1,2 @@ -limit_rate 6250k; -limit_rate_after 50k; client_max_body_size 0; proxy_request_buffering off; From 1899902860c9ac8d2b245c8fe9f8b019bcdd7ca3 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 2 Feb 2025 07:15:03 +0200 Subject: [PATCH 012/210] bunch of js === and !== fixes --- web/share/js/keypad.js | 6 +++--- web/share/js/kvm/session.js | 10 +++++----- web/share/js/kvm/stream.js | 2 +- web/share/js/kvm/stream_janus.js | 2 +- web/share/js/kvm/switch.js | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/web/share/js/keypad.js b/web/share/js/keypad.js index 07990b7c..86341722 100644 --- a/web/share/js/keypad.js +++ b/web/share/js/keypad.js @@ -102,9 +102,9 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { let code = event.code; if (__apply_fixes) { // https://github.com/pikvm/pikvm/issues/819 - if (code == "IntlBackslash" && ["`", "~"].includes(event.key)) { + if (code === "IntlBackslash" && ["`", "~"].includes(event.key)) { code = "Backquote"; - } else if (code == "Backquote" && ["§", "±"].includes(event.key)) { + } else if (code === "Backquote" && ["§", "±"].includes(event.key)) { code = "IntlBackslash"; } } @@ -128,7 +128,7 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { }; var __fixMacCmd = function(code, state) { - if ((code == "MetaLeft" || code == "MetaRight") && !state) { + if ((code === "MetaLeft" || code === "MetaRight") && !state) { for (code in __keys) { if (__isActive(__keys[code][0])) { self.emitByCode(code, false, false); diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 9e944e81..2133a885 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -319,7 +319,7 @@ export function Session() { var __ascii_encoder = new TextEncoder("ascii"); var __sendHidEvent = function(ws, event_type, event) { - if (event_type == "key") { + if (event_type === "key") { let data = __ascii_encoder.encode("\x01\x00" + event.key); data[1] = (event.state ? 1 : 0); if (event.finish === true) { // Optional @@ -327,12 +327,12 @@ export function Session() { } ws.send(data); - } else if (event_type == "mouse_button") { + } else if (event_type === "mouse_button") { let data = __ascii_encoder.encode("\x02\x00" + event.button); data[1] = (event.state ? 1 : 0); ws.send(data); - } else if (event_type == "mouse_move") { + } else if (event_type === "mouse_move") { let data = new Uint8Array([ 3, (event.to.x >> 8) & 0xFF, event.to.x & 0xFF, @@ -340,7 +340,7 @@ export function Session() { ]); ws.send(data); - } else if (event_type == "mouse_relative" || event_type == "mouse_wheel") { + } else if (event_type === "mouse_relative" || event_type === "mouse_wheel") { let data; if (Array.isArray(event.delta)) { data = new Int8Array(2 + event.delta.length * 2); @@ -353,7 +353,7 @@ export function Session() { } else { data = new Int8Array([0, 0, event.delta.x, event.delta.y]); } - data[0] = (event_type == "mouse_relative" ? 4 : 5); + data[0] = (event_type === "mouse_relative" ? 4 : 5); data[1] = (event.squash ? 1 : 0); ws.send(data); } diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index 9df3c4b6..34baaa92 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -78,7 +78,7 @@ export function Streamer() { if (__streamer.getMode() === "janus") { // Right now it's working only for H.264 let orient = parseInt(tools.radio.getValue("stream-orient-radio")); tools.storage.setInt("stream.orient", orient); - if (__streamer.getOrientation() != orient) { + if (__streamer.getOrientation() !== orient) { __resetStream(); } } diff --git a/web/share/js/kvm/stream_janus.js b/web/share/js/kvm/stream_janus.js index 3e9ea5dd..18a70005 100644 --- a/web/share/js/kvm/stream_janus.js +++ b/web/share/js/kvm/stream_janus.js @@ -297,7 +297,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _ // Chrome sends `muted` notifiation for tracks in `disconnected` ICE state // and Janus.js just removes muted track from list of available tracks. // But track still exists actually so it's safe to just ignore - // reason == "mute" and "unmute". + // reason === "mute" and "unmute". let reason = (meta || {}).reason; __logInfo("Got onremotetrack:", id, added, reason, track, meta); if (added && reason === "created") { diff --git a/web/share/js/kvm/switch.js b/web/share/js/kvm/switch.js index 1ed9adca..3fab203f 100644 --- a/web/share/js/kvm/switch.js +++ b/web/share/js/kvm/switch.js @@ -334,7 +334,7 @@ export function Switch() { let content = ""; let unit = -1; for (let port = 0; port < model.ports.length; ++port) { - let pa = model.ports[port]; // pa == port attrs + let pa = model.ports[port]; // pa === port attrs if (unit !== pa.unit) { unit = pa.unit; content += `${unit > 0 ? "


      -
      ${controls.join("")}
         
      `; - } else { - return ""; } + + return ""; }; var __setLedState = function(el, on) { diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js index 3310d17e..4acf99a3 100644 --- a/web/share/js/kvm/mouse.js +++ b/web/share/js/kvm/mouse.js @@ -112,7 +112,7 @@ export function Mouse(__getGeometry, __recordWsEvent) { }; var __updateRate = function(value) { - $("hid-mouse-rate-value").innerHTML = value + " ms"; + $("hid-mouse-rate-value").innerText = value + " ms"; tools.storage.set("hid.mouse.rate", value); if (__timer) { clearInterval(__timer); @@ -121,13 +121,13 @@ export function Mouse(__getGeometry, __recordWsEvent) { }; var __updateScrollRate = function(value) { - $("hid-mouse-scroll-value").innerHTML = value; + $("hid-mouse-scroll-value").innerText = value; tools.storage.set("hid.mouse.scroll_rate", value); __scroll_rate = value; }; var __updateRelativeSens = function(value) { - $("hid-mouse-sens-value").innerHTML = value.toFixed(1); + $("hid-mouse-sens-value").innerText = value.toFixed(1); tools.storage.set("hid.mouse.sens", value); __relative_sens = value; }; diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index 13d53079..b513c08d 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -208,7 +208,7 @@ export function Msd() { if (el.__names_json !== names_json) { el.innerHTML = names.map(name => `
      -
      +
      @@ -223,7 +223,7 @@ export function Msd() { ? `${names.length === 1 ? "Storage: %s" : "Internal storage: %s"}` // eslint-disable-line : `Storage [${name}${part.writable ? "]" : ", read-only]"}: %s` // eslint-disable-line ); - let id = `__msd-storage-${tools.makeIdByText(name)}-progress`; + let id = `__msd-storage-${tools.makeTextId(name)}-progress`; tools.progress.setSizeOf($(id), title, part.size, part.free); } }; @@ -270,8 +270,8 @@ export function Msd() { }; var __clickDownloadButton = function() { - let image = encodeURIComponent($("msd-image-selector").value); - tools.windowOpen(`api/msd/read?image=${image}`); + let e_image = encodeURIComponent($("msd-image-selector").value); + tools.windowOpen(`api/msd/read?image=${e_image}`); }; var __clickRemoveButton = function() { @@ -299,13 +299,13 @@ export function Msd() { var __clickUploadNewButton = function() { let file = tools.input.getFile($("msd-new-file")); __http = new XMLHttpRequest(); - let prefix = encodeURIComponent($("msd-new-part-selector").value); + let e_prefix = encodeURIComponent($("msd-new-part-selector").value); if (file) { - let image = encodeURIComponent(file.name); - __http.open("POST", `${ROOT_PREFIX}api/msd/write?prefix=${prefix}&image=${image}&remove_incomplete=1`, true); + let e_image = encodeURIComponent(file.name); + __http.open("POST", `${ROOT_PREFIX}api/msd/write?prefix=${e_prefix}&image=${e_image}&remove_incomplete=1`, true); } else { - let url = encodeURIComponent($("msd-new-url").value); - __http.open("POST", `${ROOT_PREFIX}api/msd/write_remote?prefix=${prefix}&url=${url}&remove_incomplete=1`, true); + let e_url = encodeURIComponent($("msd-new-url").value); + __http.open("POST", `${ROOT_PREFIX}api/msd/write_remote?prefix=${e_prefix}&url=${e_url}&remove_incomplete=1`, true); } __http.upload.timeout = 7 * 24 * 3600; __http.onreadystatechange = __uploadStateChange; @@ -402,7 +402,8 @@ export function Msd() { if (__state && __state.storage && __state.storage.parts) { let part = __state.storage.parts[$("msd-new-part-selector").value]; if (part && (file.size > part.size)) { - wm.error(`The new image is too big for the Mass Storage partition.
      Maximum: ${tools.formatSize(part.size)}`); + let e_size = tools.escape(tools.formatSize(part.size)); + wm.error(`The new image is too big for the Mass Storage partition.
      Maximum: ${e_size}`); el.value = ""; } } diff --git a/web/share/js/kvm/stream_media.js b/web/share/js/kvm/stream_media.js index 0e552677..a0ea323a 100644 --- a/web/share/js/kvm/stream_media.js +++ b/web/share/js/kvm/stream_media.js @@ -220,7 +220,8 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { let width = frame.displayWidth; let height = frame.displayHeight; switch (__orient) { - case 90: case 270: + case 90: + case 270: width = frame.displayHeight; height = frame.displayWidth; } diff --git a/web/share/js/kvm/stream_mjpeg.js b/web/share/js/kvm/stream_mjpeg.js index a13e708e..46f3827b 100644 --- a/web/share/js/kvm/stream_mjpeg.js +++ b/web/share/js/kvm/stream_mjpeg.js @@ -32,7 +32,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) { /************************************************************************/ - var __key = tools.makeId(); + var __key = tools.makeRandomId(); var __id = ""; var __fps = -1; var __state = null; @@ -91,7 +91,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) { var __setStreamInactive = function() { let old_fps = __fps; - __key = tools.makeId(); + __key = tools.makeRandomId(); __id = ""; __fps = -1; __state = null; @@ -139,7 +139,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) { __setStreamInactive(); __stopChecking(); - let path = `${ROOT_PREFIX}streamer/stream?key=${__key}`; + let path = `${ROOT_PREFIX}streamer/stream?key=${encodeURIComponent(__key)}`; if (tools.browser.is_safari || tools.browser.is_ios) { // uStreamer fix for WebKit __logInfo("Using dual_final_frames=1 to fix WebKit bugs"); diff --git a/web/share/js/kvm/switch.js b/web/share/js/kvm/switch.js index 3fab203f..e54307f2 100644 --- a/web/share/js/kvm/switch.js +++ b/web/share/js/kvm/switch.js @@ -178,8 +178,8 @@ export function Switch() { }; var __clickAddEdidButton = function() { - let create_content = function(el_parent, el_ok_button) { - tools.el.setEnabled(el_ok_button, false); + let create_content = function(el_parent, el_ok_bt) { + tools.el.setEnabled(el_ok_bt, false); el_parent.innerHTML = ` @@ -203,7 +203,7 @@ export function Switch() { el_name.oninput = el_data.oninput = function() { let name = el_name.value.replace(/\s+/g, ""); let data = el_data.value.replace(/\s+/g, ""); - tools.el.setEnabled(el_ok_button, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data))); + tools.el.setEnabled(el_ok_bt, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data))); }; }; @@ -584,7 +584,7 @@ export function Switch() { }; if ($("switch-atx-ask-switch").checked) { wm.confirm(` - Are you sure you want to press the ${button} button?
      + Are you sure you want to press the ${tools.escape(button)} button?
      Warning! This could cause data loss on the server. `).then(function(ok) { if (ok) { diff --git a/web/share/js/login/main.js b/web/share/js/login/main.js index 63fa0dec..fb962e7e 100644 --- a/web/share/js/login/main.js +++ b/web/share/js/login/main.js @@ -52,20 +52,28 @@ function __login() { let passwd = $("passwd-input").value + $("code-input").value; let body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`; tools.httpPost("api/auth/login", null, function(http) { - if (http.status === 200) { - tools.currentOpen(""); - } else if (http.status === 403) { - wm.error("Invalid credentials").then(__tryAgain); - } else { - let error = ""; - if (http.status === 400) { - try { error = JSON.parse(http.responseText)["result"]["error"]; } catch { /* Nah */ } - } - if (error === "ValidatorError") { - wm.error("Invalid characters in credentials").then(__tryAgain); - } else { - wm.error("Login error", http.responseText).then(__tryAgain); - } + switch (http.status) { + case 200: + tools.currentOpen(""); + break; + + case 403: + wm.error("Invalid credentials").then(__tryAgain); + break; + + default: { + let error = ""; + if (http.status === 400) { + try { + error = JSON.parse(http.responseText)["result"]["error"]; + } catch { /* Nah */ } + } + if (error === "ValidatorError") { + wm.error("Invalid characters in credentials").then(__tryAgain); + } else { + wm.error("Login error", http.responseText).then(__tryAgain); + } + } break; } }, body, "application/x-www-form-urlencoded"); __setEnabled(false); diff --git a/web/share/js/tools.js b/web/share/js/tools.js index 11f08735..4965a693 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -86,8 +86,11 @@ export var tools = new function() { /************************************************************************/ self.escape = function(text) { + if (typeof text !== "string") { + text = "" + text; + } return text.replace( - /[^0-9A-Za-z ]/g, + /[^-_0-9A-Za-z ]/g, ch => "&#" + ch.charCodeAt(0) + ";" ); }; @@ -100,7 +103,7 @@ export var tools = new function() { return text[0].toUpperCase() + text.slice(1); }; - self.makeId = function() { + self.makeRandomId = function() { let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let id = ""; for (let count = 0; count < 16; ++count) { @@ -109,16 +112,10 @@ export var tools = new function() { return id; }; - self.makeIdByText = function(text) { + self.makeTextId = function(text) { return btoa(text).replace("=", "_"); }; - self.getRandomInt = function(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; - }; - self.formatSize = function(size) { if (size > 0) { let index = Math.floor( Math.log(size) / Math.log(1024) ); @@ -149,6 +146,12 @@ export var tools = new function() { return remapped; }; + self.getRandomInt = function(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + /************************************************************************/ self.el = new function() { @@ -270,26 +273,34 @@ export var tools = new function() { self.radio = new function() { return { "makeItem": function(name, title, value) { + let e_id = self.escape(name) + self.makeTextId(value); return ` - - + + `; }, "setOnClick": function(name, callback, prevent_default=true) { - for (let el of $$$(`input[type="radio"][name="${name}"]`)) { + for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) { self.el.setOnClick(el, callback, prevent_default); } }, "getValue": function(name) { - return document.querySelector(`input[type="radio"][name="${name}"]:checked`).value; + return document.querySelector(`input[type="radio"][name="${CSS.escape(name)}"]:checked`).value; }, "setValue": function(name, value) { - for (let el of $$$(`input[type="radio"][name="${name}"]`)) { + for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) { el.checked = (el.value === value); } }, "clickValue": function(name, value) { - for (let el of $$$(`input[type="radio"][name="${name}"]`)) { + for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) { if (el.value === value) { el.click(); return; @@ -297,7 +308,7 @@ export var tools = new function() { } }, "setEnabled": function(name, enabled) { - for (let el of $$$(`input[type="radio"][name="${name}"]`)) { + for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) { self.el.setEnabled(el, enabled); } }, diff --git a/web/share/js/vnc/main.js b/web/share/js/vnc/main.js index 352826b7..2e60f17c 100644 --- a/web/share/js/vnc/main.js +++ b/web/share/js/vnc/main.js @@ -32,16 +32,26 @@ export function main() { function __loadKvmdInfo() { tools.httpGet("api/info", null, function(http) { - if (http.status === 200) { - let vnc_port = JSON.parse(http.responseText).result.extras.vnc.port; - $("vnc-text").innerHTML = ` - # How to connect using the Linux terminal:
      - $
      vncviewer ${window.location.hostname}::${vnc_port} - `; - } else if (http.status === 401 || http.status === 403) { - tools.currentOpen("login"); - } else { - setTimeout(__loadKvmdInfo, 1000); + switch (http.status) { + case 200: + __showKvmdInfo(JSON.parse(http.responseText).result); + break; + + case 401: + case 403: + tools.currentOpen("login"); + break; + + default: + setTimeout(__loadKvmdInfo, 1000); + break; } }); } + +function __showKvmdInfo(info) { + $("vnc-text").innerHTML = ` + # How to connect using the Linux terminal:
      + $
      vncviewer ${tools.escape(window.location.hostname + "::" + info.extras.vnc.port)} + `; +} diff --git a/web/share/js/wm.js b/web/share/js/wm.js index 162f43ba..60a323b1 100644 --- a/web/share/js/wm.js +++ b/web/share/js/wm.js @@ -42,96 +42,105 @@ function __WindowManager() { var __menu_buttons = []; var __init__ = function() { - for (let el_button of $$$("button")) { + for (let el of $$$("button")) { // XXX: Workaround for iOS Safari: // https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari - el_button.ontouchstart = function() {}; + el.ontouchstart = function() {}; } - for (let el_button of $$("menu-button")) { - el_button.parentElement.querySelector(".menu").setAttribute("tabindex", "-1"); - tools.el.setOnDown(el_button, () => __toggleMenu(el_button)); - __menu_buttons.push(el_button); + for (let el of $$("menu-button")) { + el.parentElement.querySelector(".menu").setAttribute("tabindex", "-1"); + tools.el.setOnDown(el, () => __toggleMenu(el)); + __menu_buttons.push(el); } if (!window.ResizeObserver) { tools.error("ResizeObserver not supported"); } - for (let el_window of $$("window")) { - el_window.setAttribute("tabindex", "-1"); - __makeWindowMovable(el_window); - __windows.push(el_window); + for (let el_win of $$("window")) { + el_win.setAttribute("tabindex", "-1"); + __makeWindowMovable(el_win); + __windows.push(el_win); - if (el_window.classList.contains("window-resizable") && window.ResizeObserver) { + if (el_win.classList.contains("window-resizable") && window.ResizeObserver) { new ResizeObserver(function() { // При переполнении рабочей области сократить размер окна по высоте. // По ширине оно настраивается само в CSS. let view = self.getViewGeometry(); - let rect = el_window.getBoundingClientRect(); + let rect = el_win.getBoundingClientRect(); if ((rect.bottom - rect.top) > (view.bottom - view.top)) { let ratio = (rect.bottom - rect.top) / (view.bottom - view.top); - el_window.style.height = view.bottom - view.top + "px"; - el_window.style.width = Math.round((rect.right - rect.left) / ratio) + "px"; + el_win.style.height = view.bottom - view.top + "px"; + el_win.style.width = Math.round((rect.right - rect.left) / ratio) + "px"; } - if (el_window.hasAttribute("data-centered")) { - __centerWindow(el_window); + if (el_win.hasAttribute("data-centered")) { + __centerWindow(el_win); } - }).observe(el_window); + }).observe(el_win); } - let el_close_button = el_window.querySelector(".window-header .window-button-close"); - if (el_close_button) { - el_close_button.title = "Close window"; - tools.el.setOnClick(el_close_button, () => self.closeWindow(el_window)); + { + let el = el_win.querySelector(".window-header .window-button-close"); + if (el) { + el.title = "Close window"; + tools.el.setOnClick(el, () => self.closeWindow(el_win)); + } } - let el_maximize_button = el_window.querySelector(".window-header .window-button-maximize"); - if (el_maximize_button) { - el_maximize_button.title = "Maximize window"; - tools.el.setOnClick(el_maximize_button, function() { - __maximizeWindow(el_window); - __activateLastWindow(el_window); - }); + { + let el = el_win.querySelector(".window-header .window-button-maximize"); + if (el) { + el.title = "Maximize window"; + tools.el.setOnClick(el, function() { + __maximizeWindow(el_win); + __activateLastWindow(el_win); + }); + } } - let el_orig_button = el_window.querySelector(".window-header .window-button-original"); - if (el_orig_button) { - el_orig_button.title = "Reduce window to its original size and center it"; - tools.el.setOnClick(el_orig_button, function() { - el_window.style.width = ""; - el_window.style.height = ""; - __centerWindow(el_window); - __activateLastWindow(el_window); - }); + { + let el = el_win.querySelector(".window-header .window-button-original"); + if (el) { + el.title = "Reduce window to its original size and center it"; + tools.el.setOnClick(el, function() { + el_win.style.width = ""; + el_win.style.height = ""; + __centerWindow(el_win); + __activateLastWindow(el_win); + }); + } } - let el_enter_full_tab_button = el_window.querySelector(".window-header .window-button-enter-full-tab"); - let el_exit_full_tab_button = el_window.querySelector(".window-button-exit-full-tab"); - if (el_enter_full_tab_button && el_exit_full_tab_button) { - el_enter_full_tab_button.title = "Stretch to the entire tab"; - tools.el.setOnClick(el_enter_full_tab_button, () => self.setFullTabWindow(el_window, true)); - tools.el.setOnClick(el_exit_full_tab_button, () => self.setFullTabWindow(el_window, false)); + { + let el_enter = el_win.querySelector(".window-header .window-button-enter-full-tab"); + let el_exit = el_win.querySelector(".window-button-exit-full-tab"); + if (el_enter && el_exit) { + el_enter.title = "Stretch to the entire tab"; + tools.el.setOnClick(el_enter, () => self.setFullTabWindow(el_win, true)); + tools.el.setOnClick(el_exit, () => self.setFullTabWindow(el_win, false)); + } } - let el_full_screen_button = el_window.querySelector(".window-header .window-button-full-screen"); - if (el_full_screen_button && __getFullScreenFunction(el_window)) { - el_full_screen_button.title = "Go to full-screen mode"; - tools.el.setOnClick(el_full_screen_button, function() { - __fullScreenWindow(el_window); - el_window.focus(el_window); // Почему-то теряется фокус - __activateLastWindow(el_window); - }); + { + let el = el_win.querySelector(".window-header .window-button-full-screen"); + if (el && __getFullScreenFunction(el_win)) { + el.title = "Go to full-screen mode"; + tools.el.setOnClick(el, function() { + __fullScreenWindow(el_win); + el_win.focus(el_win); // Почему-то теряется фокус + __activateLastWindow(el_win); + }); + } } } - for (let el_button of $$$("button[data-show-window]")) { - tools.el.setOnClick(el_button, () => self.showWindow($(el_button.getAttribute("data-show-window")))); + for (let el of $$$("button[data-show-window]")) { + tools.el.setOnClick(el, () => self.showWindow($(el.getAttribute("data-show-window")))); } - window.onmouseup = __globalMouseButtonHandler; - window.ontouchend = __globalMouseButtonHandler; + window.onmouseup = window.ontouchend = __globalMouseButtonHandler; window.addEventListener("focusin", (event) => __focusInOut(event, true)); window.addEventListener("focusout", (event) => __focusInOut(event, false)); @@ -196,7 +205,12 @@ function __WindowManager() { var __modalCodeDialog = function(header, html, code, ok, cancel) { let create_content = function(el_content) { if (code) { - html += `

      ${tools.escape(code)}
      `; + html += ` +

      +
      +
      ${tools.escape(code)}
      +
      + `; } el_content.innerHTML = html; }; @@ -210,49 +224,49 @@ function __WindowManager() { el_modal.className = "modal"; el_modal.style.visibility = "visible"; - let el_window = document.createElement("div"); - el_window.className = "modal-window"; - el_window.setAttribute("tabindex", "-1"); - el_modal.appendChild(el_window); + let el_win = document.createElement("div"); + el_win.className = "modal-window"; + el_win.setAttribute("tabindex", "-1"); + el_modal.appendChild(el_win); let el_header = document.createElement("div"); el_header.className = "modal-header"; el_header.innerText = header; - el_window.appendChild(el_header); + el_win.appendChild(el_header); let el_content = document.createElement("div"); el_content.className = "modal-content"; - el_window.appendChild(el_content); + el_win.appendChild(el_content); let el_buttons = document.createElement("div"); el_buttons.classList.add("modal-buttons", "buttons-row"); - el_window.appendChild(el_buttons); + el_win.appendChild(el_buttons); - let el_cancel_button = null; - let el_ok_button = null; + let el_cancel_bt = null; + let el_ok_bt = null; if (cancel) { - el_cancel_button = document.createElement("button"); - el_cancel_button.className = "row100"; - el_cancel_button.innerText = "Cancel"; - el_buttons.appendChild(el_cancel_button); + el_cancel_bt = document.createElement("button"); + el_cancel_bt.className = "row100"; + el_cancel_bt.innerText = "Cancel"; + el_buttons.appendChild(el_cancel_bt); } if (ok) { - el_ok_button = document.createElement("button"); - el_ok_button.className = "row100"; - el_ok_button.innerText = "OK"; - el_buttons.appendChild(el_ok_button); + el_ok_bt = document.createElement("button"); + el_ok_bt.className = "row100"; + el_ok_bt.innerText = "OK"; + el_buttons.appendChild(el_ok_bt); } if (ok && cancel) { - el_ok_button.className = "row50"; - el_cancel_button.className = "row50"; + el_ok_bt.className = "row50"; + el_cancel_bt.className = "row50"; } - el_window.onkeyup = function(event) { + el_win.onkeyup = function(event) { event.preventDefault(); if (ok && event.code === "Enter") { - el_ok_button.click(); + el_ok_bt.click(); } else if (cancel && event.code === "Escape") { - el_cancel_button.click(); + el_cancel_bt.click(); } }; @@ -260,7 +274,7 @@ function __WindowManager() { if (ok || cancel) { promise = new Promise(function(resolve) { function close(retval) { - __closeWindow(el_window); + __closeWindow(el_win); let index = __windows.indexOf(el_modal); if (index !== -1) { __windows.splice(index, 1); @@ -276,10 +290,10 @@ function __WindowManager() { } if (cancel) { - tools.el.setOnClick(el_cancel_button, () => close(false)); + tools.el.setOnClick(el_cancel_bt, () => close(false)); } if (ok) { - tools.el.setOnClick(el_ok_button, () => close(true)); + tools.el.setOnClick(el_ok_bt, () => close(true)); } }); } @@ -288,7 +302,7 @@ function __WindowManager() { (parent || document.fullscreenElement || document.body).appendChild(el_modal); if (typeof html === "function") { // Это должно быть здесь, потому что элемент должен иметь родителя чтобы существовать - html(el_content, el_ok_button); + html(el_content, el_ok_bt); } else { el_content.innerHTML = html; } @@ -297,26 +311,26 @@ function __WindowManager() { return promise; }; - self.showWindow = function(el_window, activate=true, center=false) { + self.showWindow = function(el_win, activate=true, center=false) { let showed = false; - if (!self.isWindowVisible(el_window)) { + if (!self.isWindowVisible(el_win)) { center = true; showed = true; } - __organizeWindow(el_window, center); - el_window.style.visibility = "visible"; + __organizeWindow(el_win, center); + el_win.style.visibility = "visible"; if (activate) { - __activateWindow(el_window); + __activateWindow(el_win); } - if (el_window.show_hook) { + if (el_win.show_hook) { if (showed) { - el_window.show_hook(); + el_win.show_hook(); } } }; - self.isWindowVisible = function(el_window) { - return (window.getComputedStyle(el_window, null).visibility !== "hidden"); + self.isWindowVisible = function(el_win) { + return (window.getComputedStyle(el_win, null).visibility !== "hidden"); }; self.getViewGeometry = function() { @@ -329,35 +343,35 @@ function __WindowManager() { }; }; - self.closeWindow = function(el_window) { - __closeWindow(el_window); - __activateLastWindow(el_window); + self.closeWindow = function(el_win) { + __closeWindow(el_win); + __activateLastWindow(el_win); }; - self.setFullTabWindow = function(el_window, enabled) { - el_window.classList.toggle("window-full-tab", enabled); - __activateLastWindow(el_window); + self.setFullTabWindow = function(el_win, enabled) { + el_win.classList.toggle("window-full-tab", enabled); + __activateLastWindow(el_win); let el_navbar = $("navbar"); if (el_navbar) { tools.hidden.setVisible(el_navbar, !enabled); } }; - var __closeWindow = function(el_window) { - el_window.focus(); - el_window.blur(); - el_window.style.visibility = "hidden"; - if (el_window.close_hook) { - el_window.close_hook(); + var __closeWindow = function(el_win) { + el_win.focus(); + el_win.blur(); + el_win.style.visibility = "hidden"; + if (el_win.close_hook) { + el_win.close_hook(); } }; var __toggleMenu = function(el_a) { let all_hidden = true; - for (let el_button of __menu_buttons) { - let el_menu = el_button.parentElement.querySelector(".menu"); - if (el_button === el_a && window.getComputedStyle(el_menu, null).visibility === "hidden") { + for (let el_bt of __menu_buttons) { + let el_menu = el_bt.parentElement.querySelector(".menu"); + if (el_bt === el_a && window.getComputedStyle(el_menu, null).visibility === "hidden") { let rect = el_menu.getBoundingClientRect(); let offset = self.getViewGeometry().right - (rect.left + el_menu.clientWidth + 2); // + 2 is ugly hack if (offset < 0) { @@ -366,13 +380,13 @@ function __WindowManager() { el_menu.style.removeProperty("right"); } - el_button.classList.add("menu-button-pressed"); + el_bt.classList.add("menu-button-pressed"); el_menu.style.visibility = "visible"; let el_focus = el_menu.querySelector("[data-focus]"); (el_focus !== null ? el_focus : el_menu).focus(); all_hidden &= false; } else { - el_button.classList.remove("menu-button-pressed"); + el_bt.classList.remove("menu-button-pressed"); el_menu.style.visibility = "hidden"; el_menu.style.removeProperty("right"); } @@ -394,9 +408,9 @@ function __WindowManager() { var __closeAllMenues = function() { document.onkeyup = null; - for (let el_button of __menu_buttons) { - let el_menu = el_button.parentElement.querySelector(".menu"); - el_button.classList.remove("menu-button-pressed"); + for (let el_bt of __menu_buttons) { + let el_menu = el_bt.parentElement.querySelector(".menu"); + el_bt.classList.remove("menu-button-pressed"); el_menu.style.visibility = "hidden"; el_menu.style.removeProperty("right"); } @@ -420,10 +434,10 @@ function __WindowManager() { && !event.target.closest(".menu-button") && !event.target.closest(".modal") ) { - for (let el_item = event.target; el_item && el_item !== document; el_item = el_item.parentNode) { - if (el_item.classList.contains("menu")) { + for (let el = event.target; el && el !== document; el = el.parentNode) { + if (el.classList.contains("menu")) { return; - } else if (el_item.hasAttribute("data-force-hide-menu")) { + } else if (el.hasAttribute("data-force-hide-menu")) { break; } } @@ -433,122 +447,122 @@ function __WindowManager() { }; var __organizeWindowsOnBrowserResize = function() { - for (let el_window of $$("window")) { - if (el_window.style.visibility === "visible") { - if (tools.browser.is_mobile && el_window.classList.contains("window-resizable")) { + for (let el_win of $$("window")) { + if (el_win.style.visibility === "visible") { + if (tools.browser.is_mobile && el_win.classList.contains("window-resizable")) { // FIXME: При смене ориентации на мобильном браузере надо сбрасывать // настройки окна стрима, поэтому тут стоит вот этот костыль - el_window.style.width = ""; - el_window.style.height = ""; + el_win.style.width = ""; + el_win.style.height = ""; } - __organizeWindow(el_window); + __organizeWindow(el_win); } } }; - var __organizeWindow = function(el_window, center=false) { + var __organizeWindow = function(el_win, center=false) { let view = self.getViewGeometry(); - let rect = el_window.getBoundingClientRect(); + let rect = el_win.getBoundingClientRect(); - if (el_window.classList.contains("window-resizable")) { + if (el_win.classList.contains("window-resizable")) { // При переполнении рабочей области сократить размер окна if ((rect.bottom - rect.top) > (view.bottom - view.top)) { let ratio = (rect.bottom - rect.top) / (view.bottom - view.top); - el_window.style.height = view.bottom - view.top + "px"; - el_window.style.width = Math.round((rect.right - rect.left) / ratio) + "px"; + el_win.style.height = view.bottom - view.top + "px"; + el_win.style.width = Math.round((rect.right - rect.left) / ratio) + "px"; } if ((rect.right - rect.left) > (view.right - view.left)) { - el_window.style.width = view.right - view.left + "px"; + el_win.style.width = view.right - view.left + "px"; } - rect = el_window.getBoundingClientRect(); + rect = el_win.getBoundingClientRect(); } - if (el_window.hasAttribute("data-centered") || center) { - __centerWindow(el_window); + if (el_win.hasAttribute("data-centered") || center) { + __centerWindow(el_win); } else { if (rect.top <= view.top) { - el_window.style.top = view.top + "px"; + el_win.style.top = view.top + "px"; } else if (rect.bottom > view.bottom) { - el_window.style.top = view.bottom - rect.height + "px"; + el_win.style.top = view.bottom - rect.height + "px"; } if (rect.left <= view.left) { - el_window.style.left = view.left + "px"; + el_win.style.left = view.left + "px"; } else if (rect.right > view.right) { - el_window.style.left = view.right - rect.width + "px"; + el_win.style.left = view.right - rect.width + "px"; } } }; - var __centerWindow = function(el_window) { + var __centerWindow = function(el_win) { let view = self.getViewGeometry(); - let rect = el_window.getBoundingClientRect(); - el_window.style.top = Math.max(view.top, Math.round((view.bottom - rect.height) / 2)) + "px"; - el_window.style.left = Math.round((view.right - rect.width) / 2) + "px"; - el_window.setAttribute("data-centered", ""); + let rect = el_win.getBoundingClientRect(); + el_win.style.top = Math.max(view.top, Math.round((view.bottom - rect.height) / 2)) + "px"; + el_win.style.left = Math.round((view.right - rect.width) / 2) + "px"; + el_win.setAttribute("data-centered", ""); }; - var __activateLastWindow = function(el_except_window=null) { - let el_last_window = null; + var __activateLastWindow = function(el_except_win=null) { + let el_last_win = null; if (document.activeElement) { - el_last_window = (document.activeElement.closest(".modal-window") || document.activeElement.closest(".window")); - if (el_last_window && window.getComputedStyle(el_last_window, null).visibility === "hidden") { - el_last_window = null; + el_last_win = (document.activeElement.closest(".modal-window") || document.activeElement.closest(".window")); + if (el_last_win && window.getComputedStyle(el_last_win, null).visibility === "hidden") { + el_last_win = null; } } - if (!el_last_window || el_last_window === el_except_window) { + if (!el_last_win || el_last_win === el_except_win) { let max_z_index = 0; - for (let el_window of __windows) { - let z_index = parseInt(window.getComputedStyle(el_window, null).zIndex) || 0; - let visibility = window.getComputedStyle(el_window, null).visibility; + for (let el_win of __windows) { + let z_index = parseInt(window.getComputedStyle(el_win, null).zIndex) || 0; + let visibility = window.getComputedStyle(el_win, null).visibility; - if (max_z_index < z_index && visibility !== "hidden" && el_window !== el_except_window) { - el_last_window = el_window; + if (max_z_index < z_index && visibility !== "hidden" && el_win !== el_except_win) { + el_last_win = el_win; max_z_index = z_index; } } } - if (el_last_window) { - tools.debug("UI: Activating last window:", el_last_window); - __activateWindow(el_last_window); + if (el_last_win) { + tools.debug("UI: Activating last window:", el_last_win); + __activateWindow(el_last_win); } else { tools.debug("UI: No last window to activation"); } }; - var __activateWindow = function(el_window) { - if (window.getComputedStyle(el_window, null).visibility !== "hidden") { + var __activateWindow = function(el_win) { + if (window.getComputedStyle(el_win, null).visibility !== "hidden") { let el_to_focus; - let el_window_contains_focus; + let el_focused; // A window which contains a focus - if (el_window.className === "modal") { - el_to_focus = el_window.querySelector(".modal-window"); - el_window_contains_focus = (document.activeElement && document.activeElement.closest(".modal-window")); + if (el_win.className === "modal") { + el_to_focus = el_win.querySelector(".modal-window"); + el_focused = (document.activeElement && document.activeElement.closest(".modal-window")); } else { // .window - el_to_focus = el_window; - el_window_contains_focus = (document.activeElement && document.activeElement.closest(".window")); + el_to_focus = el_win; + el_focused = (document.activeElement && document.activeElement.closest(".window")); } - if (el_window.className !== "modal" && parseInt(el_window.style.zIndex) !== __top_z_index) { + if (el_win.className !== "modal" && parseInt(el_win.style.zIndex) !== __top_z_index) { __top_z_index += 1; - el_window.style.zIndex = __top_z_index; - tools.debug("UI: Activated window:", el_window); + el_win.style.zIndex = __top_z_index; + tools.debug("UI: Activated window:", el_win); } - if (el_window !== el_window_contains_focus) { + if (el_win !== el_focused) { el_to_focus.focus(); - tools.debug("UI: Focused window:", el_window); + tools.debug("UI: Focused window:", el_win); } } }; - var __makeWindowMovable = function(el_window) { - let el_header = el_window.querySelector(".window-header"); - let el_grab = el_window.querySelector(".window-header .window-grab"); + var __makeWindowMovable = function(el_win) { + let el_header = el_win.querySelector(".window-header"); + let el_grab = el_win.querySelector(".window-header .window-grab"); if (el_header === null || el_grab === null) { // Для псевдоокна OCR return; @@ -559,10 +573,10 @@ function __WindowManager() { function startMoving(event) { // При перетаскивании resizable-окна за правый кран экрана оно ужимается. // Этот костыль фиксит это. - el_window.style.width = el_window.offsetWidth + "px"; + el_win.style.width = el_win.offsetWidth + "px"; __closeAllMenues(); - __activateWindow(el_window); + __activateWindow(el_win); event = (event || window.event); event.preventDefault(); @@ -580,7 +594,7 @@ function __WindowManager() { } function doMoving(event) { - el_window.removeAttribute("data-centered"); + el_win.removeAttribute("data-centered"); event = (event || window.event); event.preventDefault(); @@ -589,8 +603,8 @@ function __WindowManager() { let x = prev_pos.x - event_pos.x; let y = prev_pos.y - event_pos.y; - el_window.style.top = (el_window.offsetTop - y) + "px"; - el_window.style.left = (el_window.offsetLeft - x) + "px"; + el_win.style.top = (el_win.offsetTop - y) + "px"; + el_win.style.left = (el_win.offsetLeft - x) + "px"; prev_pos = event_pos; } @@ -613,29 +627,29 @@ function __WindowManager() { } } - el_window.setAttribute("data-centered", ""); - el_window.onmousedown = el_window.ontouchstart = () => __activateWindow(el_window); + el_win.setAttribute("data-centered", ""); + el_win.onmousedown = el_win.ontouchstart = () => __activateWindow(el_win); el_grab.onmousedown = startMoving; el_grab.ontouchstart = startMoving; }; var __onFullScreenChange = function(event) { - let el_window = event.target; + let el_win = event.target; if (!document.fullscreenElement) { - let rect = el_window.before_full_screen; + let rect = el_win.before_full_screen; if (rect) { - el_window.style.width = rect.width + "px"; - el_window.style.height = rect.height + "px"; - el_window.style.top = rect.top + "px"; - el_window.style.left = rect.left + "px"; + el_win.style.width = rect.width + "px"; + el_win.style.height = rect.height + "px"; + el_win.style.top = rect.top + "px"; + el_win.style.left = rect.left + "px"; } } }; - var __fullScreenWindow = function(el_window) { - el_window.before_full_screen = el_window.getBoundingClientRect(); - __getFullScreenFunction(el_window).call(el_window); + var __fullScreenWindow = function(el_win) { + el_win.before_full_screen = el_win.getBoundingClientRect(); + __getFullScreenFunction(el_win).call(el_win); if (navigator.keyboard && navigator.keyboard.lock) { navigator.keyboard.lock(); } else { @@ -647,26 +661,26 @@ function __WindowManager() { + "In Chrome use HTTPS and enable system-keyboard-lock
      " + "by putting at URL chrome://flags/#system-keyboard-lock" ); - __modalDialog("Keyboard lock is unsupported", msg, true, false, el_window); + __modalDialog("Keyboard lock is unsupported", msg, true, false, el_win); } }; - var __maximizeWindow = function(el_window) { + var __maximizeWindow = function(el_win) { let el_navbar = $("navbar"); let vertical_offset = (el_navbar ? el_navbar.offsetHeight : 0); - el_window.style.left = "0px"; - el_window.style.top = vertical_offset + "px"; - el_window.style.width = window.innerWidth + "px"; - el_window.style.height = window.innerHeight - vertical_offset + "px"; + el_win.style.left = "0px"; + el_win.style.top = vertical_offset + "px"; + el_win.style.width = window.innerWidth + "px"; + el_win.style.height = window.innerHeight - vertical_offset + "px"; }; - var __getFullScreenFunction = function(el_window) { - if (el_window.requestFullscreen) { - return el_window.requestFullscreen; - } else if (el_window.webkitRequestFullscreen) { - return el_window.webkitRequestFullscreen; - } else if (el_window.mozRequestFullscreen) { - return el_window.mozRequestFullscreen; + var __getFullScreenFunction = function(el_win) { + if (el_win.requestFullscreen) { + return el_win.requestFullscreen; + } else if (el_win.webkitRequestFullscreen) { + return el_win.webkitRequestFullscreen; + } else if (el_win.mozRequestFullscreen) { + return el_win.mozRequestFullscreen; } return null; }; From 54f6d93f63c588ef85022f2b046e8770c0a556ff Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 6 Feb 2025 18:40:41 +0200 Subject: [PATCH 017/210] kvmd: binary ping/pong --- kvmd/apps/kvmd/server.py | 4 +++ web/share/js/kvm/session.js | 54 +++++++++++++++++++------------- web/share/js/kvm/stream_media.js | 9 +++--- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 858ba1b6..8eecaf7f 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -254,6 +254,10 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins async def __ws_ping_handler(self, ws: WsSession, _: dict) -> None: await ws.send_event("pong", {}) + @exposed_ws(0) + async def __ws_bin_ping_handler(self, ws: WsSession, _: bytes) -> None: + await ws.send_bin(255, b"") # Ping-pong + # ===== SYSTEM STUFF def run(self, **kwargs: Any) -> None: # type: ignore # pylint: disable=arguments-differ diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 2133a885..a540a94b 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -301,8 +301,16 @@ export function Session() { if (http.status === 200) { __ws = new WebSocket(tools.makeWsUrl("api/ws")); __ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event); + __ws.binaryType = "arraybuffer"; __ws.onopen = __wsOpenHandler; - __ws.onmessage = __wsMessageHandler; + __ws.onmessage = async (event) => { + if (typeof event.data === "string") { + event = JSON.parse(event.data); + __wsJsonHandler(event.event_type, event.event); + } else { // Binary + __wsBinHandler(event.data); + } + }; __ws.onerror = __wsErrorHandler; __ws.onclose = __wsCloseHandler; } else if (http.status === 401 || http.status === 403) { @@ -369,33 +377,37 @@ export function Session() { __ping_timer = setInterval(__pingServer, 1000); }; - var __wsMessageHandler = function(event) { - // tools.debug("Session: received socket data:", event.data); - let data = JSON.parse(event.data); - switch (data.event_type) { - case "pong": __missed_heartbeats = 0; break; - case "info": __setInfoState(data.event); break; - case "gpio": __gpio.setState(data.event); break; - case "hid": __hid.setState(data.event); break; - case "hid_keymaps": __paste.setState(data.event); break; - case "atx": __atx.setState(data.event); break; - case "streamer": __streamer.setState(data.event); break; - case "ocr": __ocr.setState(data.event); break; + var __wsBinHandler = function(data) { + data = new Uint8Array(data); + if (data[0] === 255) { // Pong + __missed_heartbeats = 0; + } + }; + + var __wsJsonHandler = function(event_type, event) { + switch (event_type) { + case "info": __setInfoState(event); break; + case "gpio": __gpio.setState(event); break; + case "hid": __hid.setState(event); break; + case "hid_keymaps": __paste.setState(event); break; + case "atx": __atx.setState(event); break; + case "streamer": __streamer.setState(event); break; + case "ocr": __ocr.setState(event); break; case "msd": - if (data.event.online === false) { + if (event.online === false) { __switch.setMsdConnected(false); - } else if (data.event.drive !== undefined) { - __switch.setMsdConnected(data.event.drive.connected); + } else if (event.drive !== undefined) { + __switch.setMsdConnected(event.drive.connected); } - __msd.setState(data.event); + __msd.setState(event); break; case "switch": - if (data.event.model) { - __atx.setHasSwitch(data.event.model.ports.length > 0); + if (event.model) { + __atx.setHasSwitch(event.model.ports.length > 0); } - __switch.setState(data.event); + __switch.setState(event); break; } }; @@ -442,7 +454,7 @@ export function Session() { if (__missed_heartbeats >= 15) { throw new Error("Too many missed heartbeats"); } - __ws.send("{\"event_type\": \"ping\", \"event\": {}}"); + __ws.send(new Uint8Array([0])); } catch (ex) { __wsErrorHandler(ex.message); } diff --git a/web/share/js/kvm/stream_media.js b/web/share/js/kvm/stream_media.js index a0ea323a..0e81c7d9 100644 --- a/web/share/js/kvm/stream_media.js +++ b/web/share/js/kvm/stream_media.js @@ -87,7 +87,8 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { __ws.onclose = __wsCloseHandler; __ws.onmessage = async (event) => { if (typeof event.data === "string") { - __wsJsonHandler(JSON.parse(event.data)); + event = JSON.parse(event.data); + __wsJsonHandler(event.event_type, event.event); } else { // Binary await __wsBinHandler(event.data); } @@ -153,9 +154,9 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { } }; - var __wsJsonHandler = function(event) { - if (event.event_type === "media") { - __decoderCreate(event.event.video); + var __wsJsonHandler = function(event_type, event) { + if (event_type === "media") { + __decoderCreate(event.video); } }; From 84ec99b332b99d032f9e4dc93086ca2ec4c9e145 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 7 Feb 2025 01:10:57 +0200 Subject: [PATCH 018/210] health event instead of hw --- kvmd/apps/kvmd/api/export.py | 4 +- kvmd/apps/kvmd/api/info.py | 7 +- kvmd/apps/kvmd/info/__init__.py | 29 ++++++-- kvmd/apps/kvmd/info/{hw.py => health.py} | 62 ++-------------- kvmd/apps/kvmd/info/system.py | 58 ++++++++++++++- web/share/js/kvm/session.js | 90 +++++++++++------------- 6 files changed, 133 insertions(+), 117 deletions(-) rename kvmd/apps/kvmd/info/{hw.py => health.py} (75%) diff --git a/kvmd/apps/kvmd/api/export.py b/kvmd/apps/kvmd/api/export.py index fd672f7b..1c964b8a 100644 --- a/kvmd/apps/kvmd/api/export.py +++ b/kvmd/apps/kvmd/api/export.py @@ -57,7 +57,7 @@ class ExportApi: async def __get_prometheus_metrics(self) -> str: (atx_state, info_state, gpio_state) = await asyncio.gather(*[ self.__atx.get_state(), - self.__info_manager.get_state(["hw", "fan"]), + self.__info_manager.get_state(["health", "fan"]), self.__user_gpio.get_state(), ]) rows: list[str] = [] @@ -71,7 +71,7 @@ class ExportApi: for key in ["online", "state"]: self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}") - self.__append_prometheus_rows(rows, info_state["hw"]["health"], "pikvm_hw") # type: ignore + self.__append_prometheus_rows(rows, info_state["health"], "pikvm_hw") # type: ignore self.__append_prometheus_rows(rows, info_state["fan"], "pikvm_fan") return "\n".join(rows) diff --git a/kvmd/apps/kvmd/api/info.py b/kvmd/apps/kvmd/api/info.py index 89d45a84..8116a3fa 100644 --- a/kvmd/apps/kvmd/api/info.py +++ b/kvmd/apps/kvmd/api/info.py @@ -45,7 +45,10 @@ class InfoApi: def __valid_info_fields(self, req: Request) -> list[str]: available = self.__info_manager.get_subs() + available.add("hw") + default = set(available) + default.remove("health") return sorted(valid_info_fields( - arg=req.query.get("fields", ",".join(available)), - variants=available, + arg=req.query.get("fields", ",".join(default)), + variants=(available), ) or available) diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py index 9ede5489..99befdfd 100644 --- a/kvmd/apps/kvmd/info/__init__.py +++ b/kvmd/apps/kvmd/info/__init__.py @@ -31,7 +31,7 @@ from .auth import AuthInfoSubmanager from .system import SystemInfoSubmanager from .meta import MetaInfoSubmanager from .extras import ExtrasInfoSubmanager -from .hw import HwInfoSubmanager +from .health import HealthInfoSubmanager from .fan import FanInfoSubmanager @@ -39,11 +39,11 @@ from .fan import FanInfoSubmanager class InfoManager: def __init__(self, config: Section) -> None: self.__subs: dict[str, BaseInfoSubmanager] = { - "system": SystemInfoSubmanager(config.kvmd.streamer.cmd), + "system": SystemInfoSubmanager(config.kvmd.info.hw.platform, config.kvmd.streamer.cmd), "auth": AuthInfoSubmanager(config.kvmd.auth.enabled), "meta": MetaInfoSubmanager(config.kvmd.info.meta), "extras": ExtrasInfoSubmanager(config), - "hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()), + "health": HealthInfoSubmanager(**config.kvmd.info.hw._unpack(ignore="platform")), "fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()), } self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue() @@ -52,12 +52,29 @@ class InfoManager: return set(self.__subs) async def get_state(self, fields: (list[str] | None)=None) -> dict: - fields = (fields or list(self.__subs)) - return dict(zip(fields, await asyncio.gather(*[ + fields = set(fields or list(self.__subs)) + + hw = ("hw" in fields) # Old for compatible + system = ("system" in fields) + if hw: + fields.remove("hw") + fields.add("health") + fields.add("system") + + state = dict(zip(fields, await asyncio.gather(*[ self.__subs[field].get_state() for field in fields ]))) + if hw: + state["hw"] = { + "health": state.pop("health"), + "platform": state["system"].pop("platform"), + } + if not system: + state.pop("system") + return state + async def trigger_state(self) -> None: await asyncio.gather(*[ sub.trigger_state() @@ -70,7 +87,7 @@ class InfoManager: # - auth -- Partial # - meta -- Partial, nullable # - extras -- Partial, nullable - # - hw -- Partial + # - health -- Partial # - fan -- Partial # =========================== diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/health.py similarity index 75% rename from kvmd/apps/kvmd/info/hw.py rename to kvmd/apps/kvmd/info/health.py index 81cd1af6..db38409a 100644 --- a/kvmd/apps/kvmd/info/hw.py +++ b/kvmd/apps/kvmd/info/health.py @@ -20,7 +20,6 @@ # ========================================================================== # -import os import asyncio import copy @@ -45,59 +44,41 @@ _RetvalT = TypeVar("_RetvalT") # ===== -class HwInfoSubmanager(BaseInfoSubmanager): +class HealthInfoSubmanager(BaseInfoSubmanager): def __init__( self, - platform_path: str, vcgencmd_cmd: list[str], ignore_past: bool, state_poll: float, ) -> None: - self.__platform_path = platform_path self.__vcgencmd_cmd = vcgencmd_cmd self.__ignore_past = ignore_past self.__state_poll = state_poll - self.__dt_cache: dict[str, str] = {} - self.__notifier = aiotools.AioNotifier() async def get_state(self) -> dict: ( - base, - serial, - platform, throttling, cpu_percent, cpu_temp, mem, ) = await asyncio.gather( - self.__read_dt_file("model", upper=False), - self.__read_dt_file("serial-number", upper=True), - self.__read_platform_file(), self.__get_throttling(), self.__get_cpu_percent(), self.__get_cpu_temp(), self.__get_mem(), ) return { - "platform": { - "type": "rpi", - "base": base, - "serial": serial, - **platform, # type: ignore + "temp": { + "cpu": cpu_temp, }, - "health": { - "temp": { - "cpu": cpu_temp, - }, - "cpu": { - "percent": cpu_percent, - }, - "mem": mem, - "throttling": throttling, + "cpu": { + "percent": cpu_percent, }, + "mem": mem, + "throttling": throttling, } async def trigger_state(self) -> None: @@ -115,35 +96,6 @@ class HwInfoSubmanager(BaseInfoSubmanager): # ===== - async def __read_dt_file(self, name: str, upper: bool) -> (str | None): - if name not in self.__dt_cache: - path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name) - try: - value = (await aiotools.read_file(path)).strip(" \t\r\n\0") - self.__dt_cache[name] = (value.upper() if upper else value) - except Exception as ex: - get_logger(0).error("Can't read DT %s from %s: %s", name, path, ex) - return None - return self.__dt_cache[name] - - async def __read_platform_file(self) -> dict: - try: - text = await aiotools.read_file(self.__platform_path) - parsed: dict[str, str] = {} - for row in text.split("\n"): - row = row.strip() - if row: - (key, value) = row.split("=", 1) - parsed[key.strip()] = value.strip() - return { - "model": parsed["PIKVM_MODEL"], - "video": parsed["PIKVM_VIDEO"], - "board": parsed["PIKVM_BOARD"], - } - except Exception: - get_logger(0).exception("Can't read device model") - return {"model": None, "video": None, "board": None} - async def __get_cpu_temp(self) -> (float | None): temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp" try: diff --git a/kvmd/apps/kvmd/info/system.py b/kvmd/apps/kvmd/info/system.py index d4a450de..85b46673 100644 --- a/kvmd/apps/kvmd/info/system.py +++ b/kvmd/apps/kvmd/info/system.py @@ -28,6 +28,7 @@ from typing import AsyncGenerator from ....logging import get_logger +from .... import env from .... import aiotools from .... import aioproc @@ -38,12 +39,30 @@ from .base import BaseInfoSubmanager # ===== class SystemInfoSubmanager(BaseInfoSubmanager): - def __init__(self, streamer_cmd: list[str]) -> None: + def __init__( + self, + platform_path: str, + streamer_cmd: list[str], + ) -> None: + + self.__platform_path = platform_path self.__streamer_cmd = streamer_cmd + + self.__dt_cache: dict[str, str] = {} self.__notifier = aiotools.AioNotifier() async def get_state(self) -> dict: - streamer_info = await self.__get_streamer_info() + ( + base, + serial, + pl, + streamer_info, + ) = await asyncio.gather( + self.__read_dt_file("model", upper=False), + self.__read_dt_file("serial-number", upper=True), + self.__read_platform_file(), + self.__get_streamer_info(), + ) uname_info = platform.uname() # Uname using the internal cache return { "kvmd": {"version": __version__}, @@ -52,6 +71,12 @@ class SystemInfoSubmanager(BaseInfoSubmanager): field: getattr(uname_info, field) for field in ["system", "release", "version", "machine"] }, + "platform": { + "type": "rpi", + "base": base, + "serial": serial, + **pl, # type: ignore + }, } async def trigger_state(self) -> None: @@ -64,6 +89,35 @@ class SystemInfoSubmanager(BaseInfoSubmanager): # ===== + async def __read_dt_file(self, name: str, upper: bool) -> (str | None): + if name not in self.__dt_cache: + path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name) + try: + value = (await aiotools.read_file(path)).strip(" \t\r\n\0") + self.__dt_cache[name] = (value.upper() if upper else value) + except Exception as ex: + get_logger(0).error("Can't read DT %s from %s: %s", name, path, ex) + return None + return self.__dt_cache[name] + + async def __read_platform_file(self) -> dict: + try: + text = await aiotools.read_file(self.__platform_path) + parsed: dict[str, str] = {} + for row in text.split("\n"): + row = row.strip() + if row: + (key, value) = row.split("=", 1) + parsed[key.strip()] = value.strip() + return { + "model": parsed["PIKVM_MODEL"], + "video": parsed["PIKVM_VIDEO"], + "board": parsed["PIKVM_BOARD"], + } + except Exception: + get_logger(0).exception("Can't read device model") + return {"model": None, "video": None, "board": None} + async def __get_streamer_info(self) -> dict: version = "" features: dict[str, bool] = {} diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index a540a94b..8c31ebf6 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -58,7 +58,7 @@ export function Session() { var __ocr = new Ocr(__streamer.getGeometry); var __switch = new Switch(); - var __info_hw_state = null; + var __info_health_state = null; var __info_fan_state = null; var __init__ = function() { @@ -71,7 +71,7 @@ export function Session() { for (let key of Object.keys(state)) { switch (key) { case "meta": __setInfoStateMeta(state.meta); break; - case "hw": __setInfoStateHw(state.hw); break; + case "health": __setInfoStateHealth(state.health); break; case "fan": __setInfoStateFan(state.fan); break; case "system": __setInfoStateSystem(state.system); break; case "extras": __setInfoStateExtras(state.extras); break; @@ -91,11 +91,10 @@ export function Session() { document.title = "PiKVM Session"; } - if (state.tips && state.tips.left) { - $("kvmd-meta-tips-left").innerText = `${state.tips.left}`; - } - if (state.tips && state.tips.right) { - $("kvmd-meta-tips-right").innerText = `${state.tips.right}`; + for (let place of ["left", "right"]) { + if (state.tips && state.tips[place]) { + $(`kvmd-meta-tips-${place}`).innerText = state.tips[place]; + } } // Don't use this option, it may be removed in any time @@ -105,10 +104,10 @@ export function Session() { } }; - var __setInfoStateHw = function(state) { - if (state.health.throttling !== null) { - let flags = state.health.throttling.parsed_flags; - let ignore_past = state.health.throttling.ignore_past; + var __setInfoStateHealth = function(state) { + if (state.throttling !== null) { + let flags = state.throttling.parsed_flags; + let ignore_past = state.throttling.ignore_past; let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past)); let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past)); @@ -118,7 +117,7 @@ export function Session() { tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage); tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped); } - __info_hw_state = state; + __info_health_state = state; __renderAboutInfoHardware(); }; @@ -145,37 +144,24 @@ export function Session() { }; var __renderAboutInfoHardware = function() { - let html = ""; - if (__info_hw_state !== null) { - html += ` - Platform: - ${__formatMisc(__info_hw_state)} -
      - Temperature: - ${__formatTemp(__info_hw_state.health.temp)} -
      - Throttling: - ${__formatThrottling(__info_hw_state.health.throttling)} - `; + let parts = []; + if (__info_health_state !== null) { + parts = [ + "Resources:" + __formatMisc(__info_health_state), + "Temperature:" + __formatTemp(__info_health_state.temp), + "Throttling:" + __formatThrottling(__info_health_state.throttling), + ]; } if (__info_fan_state !== null) { - if (html.length > 0) { - html += "
      "; - } - html += ` - Fan: - ${__formatFan(__info_fan_state)} - `; + parts.push("Fan:" + __formatFan(__info_fan_state)); } - $("about-hardware").innerHTML = html; + $("about-hardware").innerHTML = parts.join("
      "); }; var __formatMisc = function(state) { return __formatUl([ - ["Base", state.platform.base], - ["Serial", state.platform.serial], - ["CPU", `${state.health.cpu.percent}%`], - ["MEM", `${state.health.mem.percent}%`], + ["CPU", `${state.cpu.percent}%`], + ["MEM", `${state.mem.percent}%`], ]); }; @@ -183,16 +169,16 @@ export function Session() { if (!state.monitored) { return __formatUl([["Status", "Not monitored"]]); } else if (state.state === null) { - return __formatUl([["Status", __colored("red", "Not available")]]); + return __formatUl([["Status", __red("Not available")]]); } else { state = state.state; let pairs = [ - ["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))], + ["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))], ["Desired speed", `${state.fan.speed}%`], ["PWM", `${state.fan.pwm}`], ]; if (state.hall.available) { - pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]); + pairs.push(["RPM", __colored(state.fan.ok, state.hall.rpm)]); } return __formatUl(pairs); } @@ -212,9 +198,9 @@ export function Session() { for (let field of Object.keys(throttling.parsed_flags).sort()) { let flags = throttling.parsed_flags[field]; let key = tools.upperFirst(field).replace("_", " "); - let value = (flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No")); + let value = (flags["now"] ? __red("RIGHT NOW") : __green("No")); if (!throttling.ignore_past) { - value += "; " + (flags["past"] ? __colored("red", "In the past") : __colored("green", "Never")); + value += "; " + (flags["past"] ? __red("In the past") : __green("Never")); } pairs.push([key, value]); } @@ -224,18 +210,17 @@ export function Session() { } }; - var __colored = function(color, html) { - return `${html}`; - }; - var __setInfoStateSystem = function(state) { $("about-version").innerHTML = ` - KVMD: ${state.kvmd.version}
      + Base: ${__commented(state.platform.base)}
      + Serial: ${__commented(state.platform.serial)}

      - Streamer: ${state.streamer.version} (${state.streamer.app}) - ${__formatStreamerFeatures(state.streamer.features)} + KVMD: ${__commented(state.kvmd.version)}

      - ${state.kernel.system} kernel: + Streamer: ${__commented(state.streamer.version + " (" + state.streamer.app + ")")}
      + ${__formatStreamerFeatures(state.streamer.features)}
      +
      + ${state.kernel.system} kernel:
      ${__formatUname(state.kernel)} `; $("kvmd-version-kvmd").innerText = state.kvmd.version; @@ -263,11 +248,16 @@ export function Session() { var __formatUl = function(pairs) { let html = ""; for (let pair of pairs) { - html += `
    • ${pair[0]}: ${pair[1]}
    • `; + html += `
    • ${pair[0]}: ${__commented(pair[1])}
    • `; } return `
        ${html}
      `; }; + var __green = (html) => __colored(true, html); + var __red = (html) => __colored(false, html); + var __colored = (ok, html) => `${html}`; + var __commented = (html) => `${html}`; + var __setInfoStateExtras = function(state) { let show_hook = null; let close_hook = null; From 0afc81f56c643f2082fd6362372065343e7aefe3 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 8 Feb 2025 18:36:27 +0200 Subject: [PATCH 019/210] ustreamer >= 6.31 --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 06e829ce..3e7969d5 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -96,7 +96,7 @@ depends=( certbot platform-io-access raspberrypi-utils - "ustreamer>=6.26" + "ustreamer>=6.31" # Systemd UDEV bug "systemd>=248.3-2" From ad019f8476a5974c0c18e1c282ee9e64a82d7ea0 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 8 Feb 2025 19:11:42 +0200 Subject: [PATCH 020/210] web: cleanup session/info code --- web/share/js/kvm/info.js | 274 ++++++++++++++++++++++++ web/share/js/kvm/session.js | 407 +++++++++--------------------------- 2 files changed, 368 insertions(+), 313 deletions(-) create mode 100644 web/share/js/kvm/info.js diff --git a/web/share/js/kvm/info.js b/web/share/js/kvm/info.js new file mode 100644 index 00000000..94831a0e --- /dev/null +++ b/web/share/js/kvm/info.js @@ -0,0 +1,274 @@ +/***************************************************************************** +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +*****************************************************************************/ + + +"use strict"; + + +import {ROOT_PREFIX} from "../vars.js"; +import {tools, $} from "../tools.js"; + + +export function Info() { + var self = this; + + /************************************************************************/ + + var __health_state = null; + var __fan_state = null; + + var __init__ = function() { + }; + + /************************************************************************/ + + self.setState = function(state) { + for (let key of Object.keys(state)) { + switch (key) { + case "meta": __setStateMeta(state.meta); break; + case "health": __setStateHealth(state.health); break; + case "fan": __setStateFan(state.fan); break; + case "system": __setStateSystem(state.system); break; + case "extras": __setStateExtras(state.extras); break; + } + } + }; + + var __setStateMeta = function(state) { + if (state !== null) { + $("kvmd-meta-json").innerText = JSON.stringify(state, undefined, 4); + + if (state.server && state.server.host) { + $("kvmd-meta-server-host").innerText = `Server: ${state.server.host}`; + document.title = `PiKVM Session: ${state.server.host}`; + } else { + $("kvmd-meta-server-host").innerText = ""; + document.title = "PiKVM Session"; + } + + for (let place of ["left", "right"]) { + if (state.tips && state.tips[place]) { + $(`kvmd-meta-tips-${place}`).innerText = state.tips[place]; + } + } + + // Don't use this option, it may be removed in any time + if (state.web && state.web.confirm_session_exit === false) { + window.onbeforeunload = null; // See main.js + } + } + }; + + var __setStateHealth = function(state) { + if (state.throttling !== null) { + let flags = state.throttling.parsed_flags; + let ignore_past = state.throttling.ignore_past; + let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past)); + let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past)); + + tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped)); + $("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden"); + $("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden"); + tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage); + tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped); + } + __health_state = state; + __renderAboutHardware(); + }; + + var __setStateFan = function(state) { + let failed = false; + let failed_past = false; + if (state.monitored) { + if (state.state === null) { + failed = true; + } else { + if (!state.state.fan.ok) { + failed = true; + } else if (state.state.fan.last_fail_ts >= 0) { + failed = true; + failed_past = true; + } + } + } + tools.hidden.setVisible($("fan-health-dropdown"), failed); + $("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden"); + + __fan_state = state; + __renderAboutHardware(); + }; + + var __renderAboutHardware = function() { + let parts = []; + if (__health_state !== null) { + parts = [ + "Resources:" + __formatMisc(__health_state), + "Temperature:" + __formatTemp(__health_state.temp), + "Throttling:" + __formatThrottling(__health_state.throttling), + ]; + } + if (__fan_state !== null) { + parts.push("Fan:" + __formatFan(__fan_state)); + } + $("about-hardware").innerHTML = parts.join("
      "); + }; + + var __formatMisc = function(state) { + return __formatUl([ + ["CPU", tools.escape(`${state.cpu.percent}%`)], + ["MEM", tools.escape(`${state.mem.percent}%`)], + ]); + }; + + var __formatFan = function(state) { + if (!state.monitored) { + return __formatUl([["Status", "Not monitored"]]); + } else if (state.state === null) { + return __formatUl([["Status", __red("Not available")]]); + } else { + state = state.state; + let pairs = [ + ["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))], + ["Desired speed", tools.escape(`${state.fan.speed}%`)], + ["PWM", tools.escape(`${state.fan.pwm}`)], + ]; + if (state.hall.available) { + pairs.push(["RPM", __colored(state.fan.ok, tools.escape(`${state.hall.rpm}`))]); + } + return __formatUl(pairs); + } + }; + + var __formatTemp = function(temp) { + let pairs = []; + for (let field of Object.keys(temp).sort()) { + pairs.push([ + tools.escape(field.toUpperCase()), + tools.escape(`${temp[field]}`) + "°C", + ]); + } + return __formatUl(pairs); + }; + + var __formatThrottling = function(throttling) { + if (throttling !== null) { + let pairs = []; + for (let field of Object.keys(throttling.parsed_flags).sort()) { + let flags = throttling.parsed_flags[field]; + let key = tools.upperFirst(field).replace("_", " "); + let value = (flags["now"] ? __red("RIGHT NOW") : __green("No")); + if (!throttling.ignore_past) { + value += "; " + (flags["past"] ? __red("In the past") : __green("Never")); + } + pairs.push([tools.escape(key), value]); + } + return __formatUl(pairs); + } else { + return "NO DATA"; + } + }; + + var __setStateSystem = function(state) { + let p = state.platform; + let s = state.streamer; + $("about-version").innerHTML = ` + Base: ${__commented(tools.escape(p.base))} +
      + Platform: ${__commented(tools.escape(p.model + "-" + p.video + "-" + p.board))} +
      + Serial: ${__commented(tools.escape(p.serial))} +
      + KVMD: ${__commented(tools.escape(state.kvmd.version))} +
      + Streamer: ${__commented(tools.escape(s.version + " (" + s.app + ")"))} + ${__formatStreamerFeatures(s.features)} +
      + ${tools.escape(state.kernel.system)} kernel: + ${__formatUname(state.kernel)} + `; + $("kvmd-version-kvmd").innerText = state.kvmd.version; + $("kvmd-version-streamer").innerText = s.version; + }; + + var __formatStreamerFeatures = function(features) { + let pairs = []; + for (let field of Object.keys(features).sort()) { + pairs.push([ + tools.escape(field), + (features[field] ? "Yes" : "No"), + ]); + } + return __formatUl(pairs); + }; + + var __formatUname = function(kernel) { + let pairs = []; + for (let field of Object.keys(kernel).sort()) { + if (field !== "system") { + pairs.push([ + tools.escape(tools.upperFirst(field)), + tools.escape(kernel[field]), + ]); + } + } + return __formatUl(pairs); + }; + + var __formatUl = function(pairs) { + let html = ""; + for (let pair of pairs) { + html += `
    • ${pair[0]}: ${__commented(pair[1])}
    • `; + } + return `
        ${html}
      `; + }; + + var __green = (html) => __colored(true, html); + var __red = (html) => __colored(false, html); + var __colored = (ok, html) => `${html}`; + var __commented = (html) => `${html}`; + + var __setStateExtras = function(state) { + let show_hook = null; + let close_hook = null; + let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started)); + if (has_webterm) { + let loc = window.location; + let base = `${loc.protocol}//${loc.host}${loc.pathname}${ROOT_PREFIX}`; + // Tailing slash after state.webterm.path is added to avoid Nginx 301 redirect + // when the location doesn't have tailing slash: "foo -> foo/". + // Reverse proxy over PiKVM can be misconfigured to handle this. + let url = base + state.webterm.path + "/?disableLeaveAlert=true"; + show_hook = function() { + tools.info("Terminal opened: ", url); + $("webterm-iframe").src = url; + }; + close_hook = function() { + tools.info("Terminal closed"); + $("webterm-iframe").src = ""; + }; + } + tools.feature.setEnabled($("system-tool-webterm"), has_webterm); + $("webterm-window").show_hook = show_hook; + $("webterm-window").close_hook = close_hook; + }; + + __init__(); +} diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 8c31ebf6..ec0aed50 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -23,10 +23,10 @@ "use strict"; -import {ROOT_PREFIX} from "../vars.js"; import {tools, $} from "../tools.js"; import {wm} from "../wm.js"; +import {Info} from "./info.js"; import {Recorder} from "./recorder.js"; import {Hid} from "./hid.js"; import {Paste} from "./paste.js"; @@ -48,6 +48,7 @@ export function Session() { var __ping_timer = null; var __missed_heartbeats = 0; + var __info = new Info(); var __streamer = new Streamer(); var __recorder = new Recorder(); var __hid = new Hid(__streamer.getGeometry, __recorder); @@ -58,231 +59,12 @@ export function Session() { var __ocr = new Ocr(__streamer.getGeometry); var __switch = new Switch(); - var __info_health_state = null; - var __info_fan_state = null; - var __init__ = function() { __streamer.ensureDeps(() => __startSession()); }; /************************************************************************/ - var __setInfoState = function(state) { - for (let key of Object.keys(state)) { - switch (key) { - case "meta": __setInfoStateMeta(state.meta); break; - case "health": __setInfoStateHealth(state.health); break; - case "fan": __setInfoStateFan(state.fan); break; - case "system": __setInfoStateSystem(state.system); break; - case "extras": __setInfoStateExtras(state.extras); break; - } - } - }; - - var __setInfoStateMeta = function(state) { - if (state !== null) { - $("kvmd-meta-json").innerText = JSON.stringify(state, undefined, 4); - - if (state.server && state.server.host) { - $("kvmd-meta-server-host").innerText = `Server: ${state.server.host}`; - document.title = `PiKVM Session: ${state.server.host}`; - } else { - $("kvmd-meta-server-host").innerText = ""; - document.title = "PiKVM Session"; - } - - for (let place of ["left", "right"]) { - if (state.tips && state.tips[place]) { - $(`kvmd-meta-tips-${place}`).innerText = state.tips[place]; - } - } - - // Don't use this option, it may be removed in any time - if (state.web && state.web.confirm_session_exit === false) { - window.onbeforeunload = null; // See main.js - } - } - }; - - var __setInfoStateHealth = function(state) { - if (state.throttling !== null) { - let flags = state.throttling.parsed_flags; - let ignore_past = state.throttling.ignore_past; - let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past)); - let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past)); - - tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped)); - $("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden"); - $("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden"); - tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage); - tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped); - } - __info_health_state = state; - __renderAboutInfoHardware(); - }; - - var __setInfoStateFan = function(state) { - let failed = false; - let failed_past = false; - if (state.monitored) { - if (state.state === null) { - failed = true; - } else { - if (!state.state.fan.ok) { - failed = true; - } else if (state.state.fan.last_fail_ts >= 0) { - failed = true; - failed_past = true; - } - } - } - tools.hidden.setVisible($("fan-health-dropdown"), failed); - $("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden"); - - __info_fan_state = state; - __renderAboutInfoHardware(); - }; - - var __renderAboutInfoHardware = function() { - let parts = []; - if (__info_health_state !== null) { - parts = [ - "Resources:" + __formatMisc(__info_health_state), - "Temperature:" + __formatTemp(__info_health_state.temp), - "Throttling:" + __formatThrottling(__info_health_state.throttling), - ]; - } - if (__info_fan_state !== null) { - parts.push("Fan:" + __formatFan(__info_fan_state)); - } - $("about-hardware").innerHTML = parts.join("
      "); - }; - - var __formatMisc = function(state) { - return __formatUl([ - ["CPU", `${state.cpu.percent}%`], - ["MEM", `${state.mem.percent}%`], - ]); - }; - - var __formatFan = function(state) { - if (!state.monitored) { - return __formatUl([["Status", "Not monitored"]]); - } else if (state.state === null) { - return __formatUl([["Status", __red("Not available")]]); - } else { - state = state.state; - let pairs = [ - ["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))], - ["Desired speed", `${state.fan.speed}%`], - ["PWM", `${state.fan.pwm}`], - ]; - if (state.hall.available) { - pairs.push(["RPM", __colored(state.fan.ok, state.hall.rpm)]); - } - return __formatUl(pairs); - } - }; - - var __formatTemp = function(temp) { - let pairs = []; - for (let field of Object.keys(temp).sort()) { - pairs.push([field.toUpperCase(), `${temp[field]}°C`]); - } - return __formatUl(pairs); - }; - - var __formatThrottling = function(throttling) { - if (throttling !== null) { - let pairs = []; - for (let field of Object.keys(throttling.parsed_flags).sort()) { - let flags = throttling.parsed_flags[field]; - let key = tools.upperFirst(field).replace("_", " "); - let value = (flags["now"] ? __red("RIGHT NOW") : __green("No")); - if (!throttling.ignore_past) { - value += "; " + (flags["past"] ? __red("In the past") : __green("Never")); - } - pairs.push([key, value]); - } - return __formatUl(pairs); - } else { - return "NO DATA"; - } - }; - - var __setInfoStateSystem = function(state) { - $("about-version").innerHTML = ` - Base: ${__commented(state.platform.base)}
      - Serial: ${__commented(state.platform.serial)}
      -
      - KVMD: ${__commented(state.kvmd.version)}
      -
      - Streamer: ${__commented(state.streamer.version + " (" + state.streamer.app + ")")}
      - ${__formatStreamerFeatures(state.streamer.features)}
      -
      - ${state.kernel.system} kernel:
      - ${__formatUname(state.kernel)} - `; - $("kvmd-version-kvmd").innerText = state.kvmd.version; - $("kvmd-version-streamer").innerText = state.streamer.version; - }; - - var __formatStreamerFeatures = function(features) { - let pairs = []; - for (let field of Object.keys(features).sort()) { - pairs.push([field, (features[field] ? "Yes" : "No")]); - } - return __formatUl(pairs); - }; - - var __formatUname = function(kernel) { - let pairs = []; - for (let field of Object.keys(kernel).sort()) { - if (field !== "system") { - pairs.push([tools.upperFirst(field), kernel[field]]); - } - } - return __formatUl(pairs); - }; - - var __formatUl = function(pairs) { - let html = ""; - for (let pair of pairs) { - html += `
    • ${pair[0]}: ${__commented(pair[1])}
    • `; - } - return `
        ${html}
      `; - }; - - var __green = (html) => __colored(true, html); - var __red = (html) => __colored(false, html); - var __colored = (ok, html) => `${html}`; - var __commented = (html) => `${html}`; - - var __setInfoStateExtras = function(state) { - let show_hook = null; - let close_hook = null; - let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started)); - if (has_webterm) { - let loc = window.location; - let base = `${loc.protocol}//${loc.host}${loc.pathname}${ROOT_PREFIX}`; - // Tailing slash after state.webterm.path is added to avoid Nginx 301 redirect - // when the location doesn't have tailing slash: "foo -> foo/". - // Reverse proxy over PiKVM can be misconfigured to handle this. - let url = base + state.webterm.path + "/?disableLeaveAlert=true"; - show_hook = function() { - tools.info("Terminal opened: ", url); - $("webterm-iframe").src = url; - }; - close_hook = function() { - tools.info("Terminal closed"); - $("webterm-iframe").src = ""; - }; - } - tools.feature.setEnabled($("system-tool-webterm"), has_webterm); - $("webterm-window").show_hook = show_hook; - $("webterm-window").close_hook = close_hook; - }; - var __startSession = function() { $("link-led").className = "led-yellow"; $("link-led").title = "Connecting..."; @@ -314,6 +96,98 @@ export function Session() { }); }; + var __wsOpenHandler = function(event) { + tools.debug("Session: socket opened:", event); + $("link-led").className = "led-green"; + $("link-led").title = "Connected"; + __recorder.setSocket(__ws); + __hid.setSocket(__ws); + __missed_heartbeats = 0; + __ping_timer = setInterval(__pingServer, 1000); + }; + + var __wsBinHandler = function(data) { + data = new Uint8Array(data); + if (data[0] === 255) { // Pong + __missed_heartbeats = 0; + } + }; + + var __wsJsonHandler = function(event_type, event) { + switch (event_type) { + case "info": __info.setState(event); break; + case "gpio": __gpio.setState(event); break; + case "hid": __hid.setState(event); break; + case "hid_keymaps": __paste.setState(event); break; + case "atx": __atx.setState(event); break; + case "streamer": __streamer.setState(event); break; + case "ocr": __ocr.setState(event); break; + + case "msd": + if (event.online === false) { + __switch.setMsdConnected(false); + } else if (event.drive !== undefined) { + __switch.setMsdConnected(event.drive.connected); + } + __msd.setState(event); + break; + + case "switch": + if (event.model) { + __atx.setHasSwitch(event.model.ports.length > 0); + } + __switch.setState(event); + break; + } + }; + + var __wsErrorHandler = function(event) { + tools.error("Session: socket error:", event); + if (__ws) { + __ws.onclose = null; + __ws.close(); + __wsCloseHandler(null); + } + }; + + var __wsCloseHandler = function(event) { + tools.debug("Session: socket closed:", event); + $("link-led").className = "led-gray"; + + if (__ping_timer) { + clearInterval(__ping_timer); + __ping_timer = null; + } + + __gpio.setState(null); + __hid.setSocket(null); // auto setState(null); + __paste.setState(null); + __atx.setState(null); + __msd.setState(null); + __streamer.setState(null); + __ocr.setState(null); + __recorder.setSocket(null); + __switch.setState(null); + __ws = null; + + setTimeout(function() { + $("link-led").className = "led-yellow"; + setTimeout(__startSession, 500); + }, 500); + }; + + var __pingServer = function() { + try { + __missed_heartbeats += 1; + if (__missed_heartbeats >= 15) { + throw new Error("Too many missed heartbeats"); + } + __ws.send(new Uint8Array([0])); + } catch (ex) { + __wsErrorHandler(ex.message); + } + }; + var __ascii_encoder = new TextEncoder("ascii"); var __sendHidEvent = function(ws, event_type, event) { @@ -357,98 +231,5 @@ export function Session() { } }; - var __wsOpenHandler = function(event) { - tools.debug("Session: socket opened:", event); - $("link-led").className = "led-green"; - $("link-led").title = "Connected"; - __recorder.setSocket(__ws); - __hid.setSocket(__ws); - __missed_heartbeats = 0; - __ping_timer = setInterval(__pingServer, 1000); - }; - - var __wsBinHandler = function(data) { - data = new Uint8Array(data); - if (data[0] === 255) { // Pong - __missed_heartbeats = 0; - } - }; - - var __wsJsonHandler = function(event_type, event) { - switch (event_type) { - case "info": __setInfoState(event); break; - case "gpio": __gpio.setState(event); break; - case "hid": __hid.setState(event); break; - case "hid_keymaps": __paste.setState(event); break; - case "atx": __atx.setState(event); break; - case "streamer": __streamer.setState(event); break; - case "ocr": __ocr.setState(event); break; - - case "msd": - if (event.online === false) { - __switch.setMsdConnected(false); - } else if (event.drive !== undefined) { - __switch.setMsdConnected(event.drive.connected); - } - __msd.setState(event); - break; - - case "switch": - if (event.model) { - __atx.setHasSwitch(event.model.ports.length > 0); - } - __switch.setState(event); - break; - } - }; - - var __wsErrorHandler = function(event) { - tools.error("Session: socket error:", event); - if (__ws) { - __ws.onclose = null; - __ws.close(); - __wsCloseHandler(null); - } - }; - - var __wsCloseHandler = function(event) { - tools.debug("Session: socket closed:", event); - - $("link-led").className = "led-gray"; - - if (__ping_timer) { - clearInterval(__ping_timer); - __ping_timer = null; - } - - __gpio.setState(null); - __hid.setSocket(null); // auto setState(null); - __paste.setState(null); - __atx.setState(null); - __msd.setState(null); - __streamer.setState(null); - __ocr.setState(null); - __recorder.setSocket(null); - __switch.setState(null); - __ws = null; - - setTimeout(function() { - $("link-led").className = "led-yellow"; - setTimeout(__startSession, 500); - }, 500); - }; - - var __pingServer = function() { - try { - __missed_heartbeats += 1; - if (__missed_heartbeats >= 15) { - throw new Error("Too many missed heartbeats"); - } - __ws.send(new Uint8Array([0])); - } catch (ex) { - __wsErrorHandler(ex.message); - } - }; - __init__(); } From ba28f035752228989a56b47b3da08201c730f865 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 8 Feb 2025 19:22:56 +0200 Subject: [PATCH 021/210] refactoring --- web/share/js/kvm/hid.js | 14 +++++++------- web/share/js/kvm/info.js | 36 ++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index e3466896..c471d4c1 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -183,13 +183,13 @@ export function Hid(__getGeometry, __recorder) { let avail_json = JSON.stringify(avail); if (el.__avail_json !== avail_json) { let html = ""; - for (let pair of [ + for (let kv of [ ["USB", "usb"], ["PS/2", "ps2"], ["Off", "disabled"], ]) { - if (avail.includes(pair[1])) { - html += tools.radio.makeItem("hid-outputs-keyboard-radio", pair[0], pair[1]); + if (avail.includes(kv[1])) { + html += tools.radio.makeItem("hid-outputs-keyboard-radio", kv[0], kv[1]); } } el.innerHTML = html; @@ -211,16 +211,16 @@ export function Hid(__getGeometry, __recorder) { if (el.__avail_json !== avail_json) { has_relative = false; let html = ""; - for (let pair of [ + for (let kv of [ ["Absolute", "usb", false], ["Abs-Win98", "usb_win98", false], ["Relative", "usb_rel", true], ["PS/2", "ps2", true], ["Off", "disabled", false], ]) { - if (avail.includes(pair[1])) { - html += tools.radio.makeItem("hid-outputs-mouse-radio", pair[0], pair[1]); - has_relative = (has_relative || pair[2]); + if (avail.includes(kv[1])) { + html += tools.radio.makeItem("hid-outputs-mouse-radio", kv[0], kv[1]); + has_relative = (has_relative || kv[2]); } } el.innerHTML = html; diff --git a/web/share/js/kvm/info.js b/web/share/js/kvm/info.js index 94831a0e..eaabc45b 100644 --- a/web/share/js/kvm/info.js +++ b/web/share/js/kvm/info.js @@ -145,32 +145,32 @@ export function Info() { return __formatUl([["Status", __red("Not available")]]); } else { state = state.state; - let pairs = [ + let kvs = [ ["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))], ["Desired speed", tools.escape(`${state.fan.speed}%`)], ["PWM", tools.escape(`${state.fan.pwm}`)], ]; if (state.hall.available) { - pairs.push(["RPM", __colored(state.fan.ok, tools.escape(`${state.hall.rpm}`))]); + kvs.push(["RPM", __colored(state.fan.ok, tools.escape(`${state.hall.rpm}`))]); } - return __formatUl(pairs); + return __formatUl(kvs); } }; var __formatTemp = function(temp) { - let pairs = []; + let kvs = []; for (let field of Object.keys(temp).sort()) { - pairs.push([ + kvs.push([ tools.escape(field.toUpperCase()), tools.escape(`${temp[field]}`) + "°C", ]); } - return __formatUl(pairs); + return __formatUl(kvs); }; var __formatThrottling = function(throttling) { if (throttling !== null) { - let pairs = []; + let kvs = []; for (let field of Object.keys(throttling.parsed_flags).sort()) { let flags = throttling.parsed_flags[field]; let key = tools.upperFirst(field).replace("_", " "); @@ -178,9 +178,9 @@ export function Info() { if (!throttling.ignore_past) { value += "; " + (flags["past"] ? __red("In the past") : __green("Never")); } - pairs.push([tools.escape(key), value]); + kvs.push([tools.escape(key), value]); } - return __formatUl(pairs); + return __formatUl(kvs); } else { return "NO DATA"; } @@ -209,33 +209,33 @@ export function Info() { }; var __formatStreamerFeatures = function(features) { - let pairs = []; + let kvs = []; for (let field of Object.keys(features).sort()) { - pairs.push([ + kvs.push([ tools.escape(field), (features[field] ? "Yes" : "No"), ]); } - return __formatUl(pairs); + return __formatUl(kvs); }; var __formatUname = function(kernel) { - let pairs = []; + let kvs = []; for (let field of Object.keys(kernel).sort()) { if (field !== "system") { - pairs.push([ + kvs.push([ tools.escape(tools.upperFirst(field)), tools.escape(kernel[field]), ]); } } - return __formatUl(pairs); + return __formatUl(kvs); }; - var __formatUl = function(pairs) { + var __formatUl = function(kvs) { let html = ""; - for (let pair of pairs) { - html += `
    • ${pair[0]}: ${__commented(pair[1])}
    • `; + for (let kv of kvs) { + html += `
    • ${kv[0]}: ${__commented(kv[1])}
    • `; } return `
        ${html}
      `; }; From abbd65a9a0bf14711dc52948b9b2c0fcc5b50f88 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 8 Feb 2025 20:01:35 +0200 Subject: [PATCH 022/210] lint fix --- kvmd/apps/kvmd/info/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py index 99befdfd..eec16c42 100644 --- a/kvmd/apps/kvmd/info/__init__.py +++ b/kvmd/apps/kvmd/info/__init__.py @@ -52,24 +52,24 @@ class InfoManager: return set(self.__subs) async def get_state(self, fields: (list[str] | None)=None) -> dict: - fields = set(fields or list(self.__subs)) + fields_set = set(fields or list(self.__subs)) - hw = ("hw" in fields) # Old for compatible - system = ("system" in fields) + hw = ("hw" in fields_set) # Old for compatible + system = ("system" in fields_set) if hw: - fields.remove("hw") - fields.add("health") - fields.add("system") + fields_set.remove("hw") + fields_set.add("health") + fields_set.add("system") - state = dict(zip(fields, await asyncio.gather(*[ + state = dict(zip(fields_set, await asyncio.gather(*[ self.__subs[field].get_state() - for field in fields + for field in fields_set ]))) if hw: state["hw"] = { - "health": state.pop("health"), - "platform": state["system"].pop("platform"), + "health": state.pop("health"), + "platform": (state["system"] or {}).pop("platform"), # {} makes mypy happy } if not system: state.pop("system") From a7c3cdc1ea8613bba786d3b0656d38d0e2ca8b6d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 8 Feb 2025 23:30:52 +0200 Subject: [PATCH 023/210] pikvm/pikvm#1204: Expire user session --- kvmd/apps/kvmd/api/auth.py | 2 + kvmd/apps/kvmd/auth.py | 64 ++++++++++++++++----- kvmd/validators/auth.py | 5 ++ testenv/tests/apps/kvmd/test_auth.py | 81 +++++++++++++++++++++++---- testenv/tests/validators/test_auth.py | 15 +++++ web/login/index.html | 19 +++++++ web/login/index.pug | 23 +++++++- web/share/js/login/main.js | 73 ++++++++++++++---------- 8 files changed, 225 insertions(+), 57 deletions(-) diff --git a/kvmd/apps/kvmd/api/auth.py b/kvmd/apps/kvmd/api/auth.py index dee4a85d..da4b0be9 100644 --- a/kvmd/apps/kvmd/api/auth.py +++ b/kvmd/apps/kvmd/api/auth.py @@ -34,6 +34,7 @@ from ....htserver import set_request_auth_info from ....validators.auth import valid_user from ....validators.auth import valid_passwd +from ....validators.auth import valid_expire from ....validators.auth import valid_auth_token from ..auth import AuthManager @@ -91,6 +92,7 @@ class AuthApi: token = await self.__auth_manager.login( user=valid_user(credentials.get("user", "")), passwd=valid_passwd(credentials.get("passwd", "")), + expire=valid_expire(credentials.get("expire", "0")), ) if token: return make_json_response(set_cookies={_COOKIE_AUTH_TOKEN: token}) diff --git a/kvmd/apps/kvmd/auth.py b/kvmd/apps/kvmd/auth.py index bf979836..706e770b 100644 --- a/kvmd/apps/kvmd/auth.py +++ b/kvmd/apps/kvmd/auth.py @@ -20,6 +20,9 @@ # ========================================================================== # +import dataclasses +import time + import secrets import pyotp @@ -34,6 +37,17 @@ from ...htserver import HttpExposed # ===== +@dataclasses.dataclass(frozen=True) +class _Session: + user: str + expire_ts: int + + def __post_init__(self) -> None: + assert self.user.strip() + assert self.user + assert self.expire_ts >= 0 + + class AuthManager: def __init__( self, @@ -72,7 +86,7 @@ class AuthManager: self.__totp_secret_path = totp_secret_path - self.__tokens: dict[str, str] = {} # {token: user} + self.__sessions: dict[str, _Session] = {} # {token: session} def is_auth_enabled(self) -> bool: return self.__enabled @@ -106,20 +120,26 @@ class AuthManager: service = self.__internal_service ok = (await service.authorize(user, passwd)) + pname = service.get_plugin_name() if ok: - get_logger().info("Authorized user %r via auth service %r", user, service.get_plugin_name()) + get_logger().info("Authorized user %r via auth service %r", user, pname) else: - get_logger().error("Got access denied for user %r from auth service %r", user, service.get_plugin_name()) + get_logger().error("Got access denied for user %r from auth service %r", user, pname) return ok - async def login(self, user: str, passwd: str) -> (str | None): + async def login(self, user: str, passwd: str, expire: int) -> (str | None): assert user == user.strip() assert user + assert expire >= 0 assert self.__enabled if (await self.authorize(user, passwd)): token = self.__make_new_token() - self.__tokens[token] = user - get_logger().info("Logged in user %r", user) + session = _Session( + user=user, + expire_ts=(0 if expire <= 0 else (self.__get_now_ts() + expire)), + ) + self.__sessions[token] = session + get_logger().info("Logged in user %r (expire_ts=%d)", session.user, session.expire_ts) return token else: return None @@ -127,24 +147,40 @@ class AuthManager: def __make_new_token(self) -> str: for _ in range(10): token = secrets.token_hex(32) - if token not in self.__tokens: + if token not in self.__sessions: return token raise AssertionError("Can't generate new unique token") + def __get_now_ts(self) -> int: + return int(time.monotonic()) + def logout(self, token: str) -> None: assert self.__enabled - if token in self.__tokens: - user = self.__tokens[token] + if token in self.__sessions: + user = self.__sessions[token].user count = 0 - for (r_token, r_user) in list(self.__tokens.items()): - if r_user == user: + for (key_t, session) in list(self.__sessions.items()): + if session.user == user: count += 1 - del self.__tokens[r_token] - get_logger().info("Logged out user %r (%d)", user, count) + del self.__sessions[key_t] + get_logger().info("Logged out user %r (was=%d)", user, count) def check(self, token: str) -> (str | None): assert self.__enabled - return self.__tokens.get(token) + session = self.__sessions.get(token) + if session is not None: + if session.expire_ts <= 0: + # Infinite session + assert session.user + return session.user + else: + # Limited session + if self.__get_now_ts() < session.expire_ts: + assert session.user + return session.user + else: + del self.__sessions[token] + return None @aiotools.atomic_fg async def cleanup(self) -> None: diff --git a/kvmd/validators/auth.py b/kvmd/validators/auth.py index 33cad456..d07a3d63 100644 --- a/kvmd/validators/auth.py +++ b/kvmd/validators/auth.py @@ -23,6 +23,7 @@ from typing import Any from .basic import valid_string_list +from .basic import valid_number from . import check_re_match @@ -40,5 +41,9 @@ def valid_passwd(arg: Any) -> str: return check_re_match(arg, "passwd characters", r"^[\x20-\x7e]*\Z$", strip=False, hide=True) +def valid_expire(arg: Any) -> int: + return int(valid_number(arg, min=0, name="expiration time")) + + def valid_auth_token(arg: Any) -> str: return check_re_match(arg, "auth token", r"^[0-9a-f]{64}$", hide=True) diff --git a/testenv/tests/apps/kvmd/test_auth.py b/testenv/tests/apps/kvmd/test_auth.py index 4fa1c8ae..d6183a39 100644 --- a/testenv/tests/apps/kvmd/test_auth.py +++ b/testenv/tests/apps/kvmd/test_auth.py @@ -21,6 +21,7 @@ import os +import asyncio import contextlib from typing import AsyncGenerator @@ -79,6 +80,64 @@ async def _get_configured_manager( # ===== +@pytest.mark.asyncio +async def test_ok__expire(tmpdir) -> None: # type: ignore + path = os.path.abspath(str(tmpdir.join("htpasswd"))) + + htpasswd = passlib.apache.HtpasswdFile(path, new=True) + htpasswd.set_password("admin", "pass") + htpasswd.save() + + async with _get_configured_manager([], path) as manager: + assert manager.is_auth_enabled() + assert manager.is_auth_required(_E_AUTH) + assert manager.is_auth_required(_E_UNAUTH) + assert not manager.is_auth_required(_E_FREE) + + assert manager.check("xxx") is None + manager.logout("xxx") + + assert (await manager.login("user", "foo", 3)) is None + assert (await manager.login("admin", "foo", 3)) is None + assert (await manager.login("user", "pass", 3)) is None + + token1 = await manager.login("admin", "pass", 3) + assert isinstance(token1, str) + assert len(token1) == 64 + + token2 = await manager.login("admin", "pass", 3) + assert isinstance(token2, str) + assert len(token2) == 64 + assert token1 != token2 + + assert manager.check(token1) == "admin" + assert manager.check(token2) == "admin" + assert manager.check("foobar") is None + + manager.logout(token1) + + assert manager.check(token1) is None + assert manager.check(token2) is None + assert manager.check("foobar") is None + + token3 = await manager.login("admin", "pass", 3) + assert isinstance(token3, str) + assert len(token3) == 64 + assert token1 != token3 + assert token2 != token3 + + await asyncio.sleep(4) + + assert manager.check(token1) is None + assert manager.check(token2) is None + assert manager.check(token3) is None + + # Check for removed token + assert manager.check(token1) is None + assert manager.check(token2) is None + assert manager.check(token3) is None + + @pytest.mark.asyncio async def test_ok__internal(tmpdir) -> None: # type: ignore path = os.path.abspath(str(tmpdir.join("htpasswd"))) @@ -96,15 +155,15 @@ async def test_ok__internal(tmpdir) -> None: # type: ignore assert manager.check("xxx") is None manager.logout("xxx") - assert (await manager.login("user", "foo")) is None - assert (await manager.login("admin", "foo")) is None - assert (await manager.login("user", "pass")) is None + assert (await manager.login("user", "foo", 0)) is None + assert (await manager.login("admin", "foo", 0)) is None + assert (await manager.login("user", "pass", 0)) is None - token1 = await manager.login("admin", "pass") + token1 = await manager.login("admin", "pass", 0) assert isinstance(token1, str) assert len(token1) == 64 - token2 = await manager.login("admin", "pass") + token2 = await manager.login("admin", "pass", 0) assert isinstance(token2, str) assert len(token2) == 64 assert token1 != token2 @@ -119,7 +178,7 @@ async def test_ok__internal(tmpdir) -> None: # type: ignore assert manager.check(token2) is None assert manager.check("foobar") is None - token3 = await manager.login("admin", "pass") + token3 = await manager.login("admin", "pass", 0) assert isinstance(token3, str) assert len(token3) == 64 assert token1 != token3 @@ -147,17 +206,17 @@ async def test_ok__external(tmpdir) -> None: # type: ignore assert manager.is_auth_required(_E_UNAUTH) assert not manager.is_auth_required(_E_FREE) - assert (await manager.login("local", "foobar")) is None - assert (await manager.login("admin", "pass2")) is None + assert (await manager.login("local", "foobar", 0)) is None + assert (await manager.login("admin", "pass2", 0)) is None - token = await manager.login("admin", "pass1") + token = await manager.login("admin", "pass1", 0) assert token is not None assert manager.check(token) == "admin" manager.logout(token) assert manager.check(token) is None - token = await manager.login("user", "foobar") + token = await manager.login("user", "foobar", 0) assert token is not None assert manager.check(token) == "user" @@ -212,7 +271,7 @@ async def test_ok__disabled() -> None: await manager.authorize("admin", "admin") with pytest.raises(AssertionError): - await manager.login("admin", "admin") + await manager.login("admin", "admin", 0) with pytest.raises(AssertionError): manager.logout("xxx") diff --git a/testenv/tests/validators/test_auth.py b/testenv/tests/validators/test_auth.py index d84e029b..0f57889c 100644 --- a/testenv/tests/validators/test_auth.py +++ b/testenv/tests/validators/test_auth.py @@ -28,6 +28,7 @@ from kvmd.validators import ValidatorError from kvmd.validators.auth import valid_user from kvmd.validators.auth import valid_users_list from kvmd.validators.auth import valid_passwd +from kvmd.validators.auth import valid_expire from kvmd.validators.auth import valid_auth_token @@ -109,6 +110,20 @@ def test_fail__valid_passwd(arg: Any) -> None: print(valid_passwd(arg)) +# ===== +@pytest.mark.parametrize("arg", ["0 ", 0, 1, 13]) +def test_ok__valid_expire(arg: Any) -> None: + value = valid_expire(arg) + assert type(value) is int # pylint: disable=unidiomatic-typecheck + assert value == int(str(arg).strip()) + + +@pytest.mark.parametrize("arg", ["test", "", None, -1, -13, 1.1]) +def test_fail__valid_expire(arg: Any) -> None: + with pytest.raises(ValidatorError): + print(valid_expire(arg)) + + # ===== @pytest.mark.parametrize("arg", [ ("0" * 64) + " ", diff --git a/web/login/index.html b/web/login/index.html index a8cbedd9..ca9a3bf0 100644 --- a/web/login/index.html +++ b/web/login/index.html @@ -37,6 +37,7 @@ +