v0.5.0: Theme system, ARM64 builds, Docker fixes, dependency updates

Theme system:
- 6 built-in presets (dark, light, high-contrast, terminal, nord, corporate)
- Admin configures preset + per-color overrides in [theme] config section
- Client-side theme switching via localStorage (flash-free)
- All static pages updated with 28 CSS custom properties

Proxy telemetry:
- Track which side terminated connection (guacd/browser/cancelled)
- Timing-based log levels (guacd close <5s = warning)
- Clamp session dimensions to safe ranges (width 640-8192, height 480-8192, DPI 16-384)

Docker fixes (#37):
- Fix port mismatch: Dockerfile now uses 8089 consistently
- Auto-generate admin API key on first run
- Add API key setup docs and recordings volume to compose example

ARM64 support:
- Multi-platform Docker builds (linux/amd64 + linux/arm64)
- Native ARM64 .deb and tarball builds via ubuntu-24.04-arm runner

Dependency updates:
- rustls 0.23.37, chrono 0.4.44, clap 4.5.60, toml 1.0.3
- futures-util 0.3.32, uuid 1.21.0, pulldown-cmark 0.13.1
- actions/upload-artifact v7, actions/download-artifact v8

Also: FreeRDP 3.x NULL deref patch (003), .gitignore for .playwright-mcp/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave Kempe
2026-03-01 15:05:41 +11:00
parent f58695c7eb
commit ea72c52a31
22 changed files with 1238 additions and 310 deletions
+128 -9
View File
@@ -79,7 +79,7 @@ jobs:
cp ../*.deb .
- name: Upload .deb artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: deb-package
path: "*.deb"
@@ -109,7 +109,7 @@ jobs:
"rustguac-${TAG_VERSION}-linux-amd64"
- name: Upload tarball artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: tarball
path: rustguac-*-linux-amd64.tar.gz
@@ -179,14 +179,115 @@ jobs:
cp "$RPMBUILD_DIR"/RPMS/*/*.rpm .
- name: Upload .rpm artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: rpm-package
path: "*.rpm"
if-no-files-found: error
# ---------------------------------------------------------------------------
# Build and push Docker image
# Build .deb + tarball (ARM64 / Debian 13)
# ---------------------------------------------------------------------------
build-deb-arm64:
name: Build .deb (arm64)
runs-on: ubuntu-24.04-arm
container: debian:trixie
steps:
- uses: actions/checkout@v6
- name: Install build dependencies
run: |
apt-get update
apt-get install -y --no-install-recommends \
curl ca-certificates gcc g++ pkg-config make git \
autoconf automake libtool \
libcairo2-dev libjpeg-dev libpng-dev libwebp-dev \
libssh2-1-dev libssl-dev libvncserver-dev \
libpango1.0-dev libpulse-dev \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
libcunit1-dev libtelnet-dev libwebsockets-dev \
uuid-dev freerdp3-dev \
dpkg-dev debhelper fakeroot build-essential
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Clone and build guacd
run: |
git clone --depth 1 $GUACD_REPO /tmp/guacamole-server
for patch in patches/*.patch; do
[ -f "$patch" ] || continue
echo "Applying: $patch"
git -C /tmp/guacamole-server apply "$GITHUB_WORKSPACE/$patch"
done
cd /tmp/guacamole-server && autoreconf -fi
mkdir /tmp/guacd-build && cd /tmp/guacd-build
/tmp/guacamole-server/configure \
--prefix=/opt/rustguac \
--with-ssh --with-vnc --with-rdp \
--without-telnet --without-kubernetes \
--disable-guacenc --disable-guaclog --disable-static
make -j"$(nproc)"
make DESTDIR="$GITHUB_WORKSPACE/debian/staging" install
- name: Build rustguac
run: $HOME/.cargo/bin/cargo build --release
- name: Build .deb package
run: |
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
GIT_HASH=$(echo "$GITHUB_SHA" | cut -c1-7)
DEB_VERSION="${VERSION}+g${GIT_HASH}"
cat > debian/changelog <<EOF
rustguac (${DEB_VERSION}) unstable; urgency=medium
* Release ${GITHUB_REF_NAME}
-- rustguac build <rustguac@localhost> $(date -R)
EOF
dpkg-buildpackage -us -uc -b
cp ../*.deb .
- name: Upload .deb artifact
uses: actions/upload-artifact@v7
with:
name: deb-package-arm64
path: "*.deb"
if-no-files-found: error
- name: Build release tarball
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
TARBALL_DIR="/tmp/rustguac-${TAG_VERSION}-linux-arm64"
mkdir -p "$TARBALL_DIR/bin" "$TARBALL_DIR/sbin" "$TARBALL_DIR/lib" "$TARBALL_DIR/static" "$TARBALL_DIR/systemd"
cp target/release/rustguac "$TARBALL_DIR/bin/"
cp debian/staging/opt/rustguac/sbin/guacd "$TARBALL_DIR/sbin/"
cp -a debian/staging/opt/rustguac/lib/*.so* "$TARBALL_DIR/lib/"
cp -r static/* "$TARBALL_DIR/static/"
cp debian/config.toml.default "$TARBALL_DIR/config.toml.default"
cp debian/rustguac.service "$TARBALL_DIR/systemd/"
cp debian/rustguac-guacd.service "$TARBALL_DIR/systemd/"
cp install-release.sh "$TARBALL_DIR/install.sh"
chmod +x "$TARBALL_DIR/install.sh"
[ -d scripts ] && cp -r scripts "$TARBALL_DIR/" || true
cd /tmp
tar czf "$GITHUB_WORKSPACE/rustguac-${TAG_VERSION}-linux-arm64.tar.gz" \
"rustguac-${TAG_VERSION}-linux-arm64"
- name: Upload tarball artifact
uses: actions/upload-artifact@v7
with:
name: tarball-arm64
path: rustguac-*-linux-arm64.tar.gz
if-no-files-found: error
# ---------------------------------------------------------------------------
# Build and push Docker image (amd64 + arm64)
# ---------------------------------------------------------------------------
build-docker:
name: Build Docker image
@@ -195,6 +296,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Set up QEMU (for ARM64 cross-build)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -208,10 +312,11 @@ jobs:
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Build and push
- name: Build and push (amd64 + arm64)
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
sol1/rustguac:${{ steps.version.outputs.version }}
@@ -224,35 +329,49 @@ jobs:
# ---------------------------------------------------------------------------
release:
name: Create Release
needs: [build-deb, build-rpm, build-docker]
needs: [build-deb, build-deb-arm64, build-rpm, build-docker]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Download deb artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: deb-package
path: artifacts/deb-package
- name: Download rpm artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: rpm-package
path: artifacts/rpm-package
- name: Download tarball artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: tarball
path: artifacts/tarball
- name: Download deb artifact (arm64)
uses: actions/download-artifact@v8
with:
name: deb-package-arm64
path: artifacts/deb-package-arm64
- name: Download tarball artifact (arm64)
uses: actions/download-artifact@v8
with:
name: tarball-arm64
path: artifacts/tarball-arm64
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
artifacts/deb-package/*.deb
artifacts/deb-package-arm64/*.deb
artifacts/rpm-package/*.rpm
artifacts/tarball/*.tar.gz
artifacts/tarball-arm64/*.tar.gz
+3
View File
@@ -45,5 +45,8 @@ debian/rustguac.substvars
rpm/staging/
*.rpm
# Playwright MCP artifacts
.playwright-mcp/
# Claude Code project memory
.claude/
Generated
+310 -125
View File
@@ -103,7 +103,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -114,14 +114,20 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
name = "arc-swap"
version = "1.8.0"
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arc-swap"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
dependencies = [
"rustversion",
]
@@ -202,9 +208,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.15.4"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
@@ -213,9 +219,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.37.0"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
dependencies = [
"cc",
"cmake",
@@ -326,9 +332,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bcrypt-pbkdf"
@@ -343,9 +349,9 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "blake2"
@@ -367,9 +373,9 @@ dependencies = [
[[package]]
name = "block-buffer"
version = "0.11.0"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
"hybrid-array",
]
@@ -395,9 +401,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.1"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byteorder"
@@ -422,9 +428,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.55"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -457,9 +463,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.43"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -481,9 +487,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.58"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
@@ -491,9 +497,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.58"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
@@ -641,9 +647,9 @@ dependencies = [
[[package]]
name = "crypto-common"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331"
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
"hybrid-array",
]
@@ -808,9 +814,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.5.5"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
"serde_core",
@@ -830,13 +836,13 @@ dependencies = [
[[package]]
name = "digest"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6"
checksum = "285743a676ccb6b3e116bc14cc69319b957867930ae9c4822f8e0f54509d7243"
dependencies = [
"block-buffer 0.11.0",
"block-buffer 0.12.0",
"const-oid 0.10.2",
"crypto-common 0.2.0",
"crypto-common 0.2.1",
]
[[package]]
@@ -953,7 +959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1006,6 +1012,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -1033,9 +1045,9 @@ dependencies = [
[[package]]
name = "fs-err"
version = "3.2.2"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7"
checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0"
dependencies = [
"autocfg",
"tokio",
@@ -1049,9 +1061,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
@@ -1064,9 +1076,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
@@ -1074,15 +1086,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
@@ -1091,15 +1103,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
@@ -1108,15 +1120,15 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-timer"
@@ -1126,9 +1138,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
@@ -1138,7 +1150,6 @@ dependencies = [
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
@@ -1200,6 +1211,19 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "ghash"
version = "0.5.1"
@@ -1275,6 +1299,15 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1283,7 +1316,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
"foldhash 0.2.0",
]
[[package]]
@@ -1492,14 +1525,13 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.19"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
@@ -1619,6 +1651,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -1687,7 +1725,7 @@ checksum = "fe44f2bbd99fcb302e246e2d6bcf51aeda346d02a365f80296a07a8c711b6da6"
dependencies = [
"argon2",
"bcrypt-pbkdf",
"digest 0.11.0",
"digest 0.11.1",
"ecdsa",
"ed25519-dalek",
"hex",
@@ -1765,9 +1803,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.85"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1782,6 +1820,12 @@ dependencies = [
"spin",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.182"
@@ -1921,9 +1965,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.7.6"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mime"
@@ -2008,7 +2052,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2305,18 +2349,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
@@ -2325,9 +2369,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
@@ -2452,6 +2496,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "primeorder"
version = "0.13.6"
@@ -2494,9 +2548,9 @@ dependencies = [
[[package]]
name = "pulldown-cmark"
version = "0.13.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6"
dependencies = [
"bitflags",
"getopts",
@@ -2715,9 +2769,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -2726,9 +2780,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.8"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
@@ -2822,7 +2876,7 @@ dependencies = [
"const-oid 0.10.2",
"crypto-bigint 0.7.0-rc.18",
"crypto-primes",
"digest 0.11.0",
"digest 0.11.1",
"pkcs1 0.8.0-rc.4",
"pkcs8 0.11.0-rc.11",
"rand_core 0.10.0-rc-3",
@@ -2859,9 +2913,9 @@ dependencies = [
[[package]]
name = "russh"
version = "0.57.0"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01fe22d10a0e39c1134a971d5b8db8a40357b48ef22d81fa8d6eac22202dd782"
checksum = "afe62631a04a1f4d71a14b99505483b95ff97c503b67d876c042fce659186956"
dependencies = [
"aes",
"aws-lc-rs",
@@ -2961,7 +3015,7 @@ dependencies = [
[[package]]
name = "rustguac"
version = "0.4.1"
version = "0.5.0"
dependencies = [
"axum",
"axum-server",
@@ -3013,9 +3067,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.36"
version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"aws-lc-rs",
"log",
@@ -3066,9 +3120,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
@@ -3236,9 +3290,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.16.1"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -3255,9 +3309,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.16.1"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
dependencies = [
"darling",
"proc-macro2",
@@ -3294,7 +3348,7 @@ checksum = "3b167252f3c126be0d8926639c4c4706950f01445900c4b3db0fd7e89fcb750a"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.11.0",
"digest 0.11.1",
]
[[package]]
@@ -3316,7 +3370,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.11.0",
"digest 0.11.1",
]
[[package]]
@@ -3360,7 +3414,7 @@ version = "3.0.0-rc.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a96996ccff7dfa16f052bd995b4cecc72af22c35138738dc029f0ead6608d"
dependencies = [
"digest 0.11.0",
"digest 0.11.1",
"rand_core 0.10.0-rc-3",
]
@@ -3509,9 +3563,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.114"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -3740,9 +3794,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.0.1+spec-1.1.0"
version = "1.0.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220"
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
dependencies = [
"indexmap 2.13.0",
"serde_core",
@@ -3764,9 +3818,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.8+spec-1.1.0"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
]
@@ -3779,9 +3833,9 @@ checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tonic"
version = "0.14.3"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a"
checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
dependencies = [
"async-trait",
"axum",
@@ -3997,9 +4051,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.22"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
@@ -4007,6 +4061,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
@@ -4068,11 +4128,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.20.0"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
"getrandom 0.3.4",
"getrandom 0.4.1",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -4121,10 +4181,19 @@ dependencies = [
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
@@ -4135,9 +4204,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.58"
version = "0.4.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
dependencies = [
"cfg-if",
"futures-util",
@@ -4149,9 +4218,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4159,9 +4228,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.108"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -4172,18 +4241,52 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.108"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.85"
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap 2.13.0",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap 2.13.0",
"semver",
]
[[package]]
name = "web-sys"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4507,6 +4610,88 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap 2.13.0",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap 2.13.0",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap 2.13.0",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"
@@ -4566,18 +4751,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.37"
version = "0.8.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.37"
version = "0.8.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
dependencies = [
"proc-macro2",
"quote",
@@ -4660,6 +4845,6 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.18"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustguac"
version = "0.4.1"
version = "0.5.0"
edition = "2021"
description = "Lightweight Rust replacement for Apache Guacamole client"
+13 -3
View File
@@ -10,7 +10,7 @@
# docker build -t rustguac .
#
# Run:
# docker run -d -p 8080:8080 rustguac
# docker run -d -p 8089:8089 rustguac
#
# The image runs both guacd and rustguac under a simple entrypoint script.
# =============================================================================
@@ -114,7 +114,7 @@ RUN /opt/rustguac/bin/rustguac generate-cert --hostname localhost --out-dir /opt
# Default config (guacd TLS enabled by default)
RUN cat > /opt/rustguac/config.toml <<'EOF'
listen_addr = "0.0.0.0:8080"
listen_addr = "0.0.0.0:8089"
guacd_addr = "127.0.0.1:4822"
recording_path = "/opt/rustguac/recordings"
static_path = "/opt/rustguac/static"
@@ -136,6 +136,16 @@ RUN cat > /opt/rustguac/entrypoint.sh <<'SCRIPT'
#!/bin/sh
set -e
# Create admin API key on first run (if no DB exists yet)
DB_PATH="/opt/rustguac/data/rustguac.db"
if [ ! -f "$DB_PATH" ]; then
echo "First run detected — creating admin API key..."
/opt/rustguac/bin/rustguac --config /opt/rustguac/config.toml add-admin --name docker-admin
echo ""
echo "==> SAVE THE API KEY ABOVE — it is only shown once! <=="
echo ""
fi
# Start guacd in background
echo "Starting guacd..."
LD_LIBRARY_PATH=/opt/rustguac/lib /opt/rustguac/sbin/guacd \
@@ -161,7 +171,7 @@ SCRIPT
RUN chmod +x /opt/rustguac/entrypoint.sh
WORKDIR /opt/rustguac
EXPOSE 8080
EXPOSE 8089
VOLUME ["/opt/rustguac/data", "/opt/rustguac/recordings"]
ENV RUST_LOG=info
+34
View File
@@ -178,6 +178,40 @@ web_allowed_networks = ["127.0.0.0/8", "::1/128"]
# max_recordings = 0 # global max recording count (0 = unlimited)
# rotation_interval_secs = 300 # how often to check rotation (seconds, default: 300)
# ─── Theme ────────────────────────────────────────────────────────────────────
#
# Customize the web UI appearance. Pick a built-in preset and optionally
# override individual colors. Users can switch themes in the browser via
# the theme switcher (persisted in localStorage).
#
# Built-in presets: dark (default), light, high-contrast, terminal, nord, corporate
#
# All color values are CSS color strings (hex, rgb(), hsl(), named colors).
# [theme]
# preset = "dark" # built-in preset to use as the base
# logo_url = "/logo.svg" # custom logo URL (shown on login + nav)
#
# # Override individual colors on top of the preset:
# # primary_color = "#e94560"
# # accent_color = "#5bc0be"
# # bg_color = "#1a1a2e"
# # surface_color = "#16213e"
# # text_color = "#e0e0e0"
# # text_muted = "#aaa"
# # border_color = "#333"
# # input_color = "#0f3460"
# # primary_hover = "#c73652"
# # accent_hover = "#4aa3a1"
# # text_dim = "#888"
# # text_on_primary = "#fff"
# # btn_disabled = "#555"
# # status_pending = "#f0c040"
# # status_active = "#5bc0be"
# # status_completed = "#888"
# # status_error = "#e94560"
# # status_expired = "#666"
# ─── Drive / File Transfer ──────────────────────────────────────────────────
#
# Enables file transfer for RDP (drive redirection) and SSH (SFTP) sessions.
+17
View File
@@ -118,6 +118,21 @@ The Docker image:
- Enables TLS between rustguac and guacd by default
- Exposes HTTP on port 8089 (put a reverse proxy in front for HTTPS)
### API key setup
On first run (when no database exists), the container automatically generates an admin API key and prints it to the logs:
```bash
docker logs rustguac
```
Save the printed key — it is only shown once. To generate additional keys later:
```bash
docker exec rustguac /opt/rustguac/bin/rustguac \
--config /opt/rustguac/config.toml add-admin --name my-admin
```
### Docker Compose example
```yaml
@@ -128,11 +143,13 @@ services:
- "8089:8089"
volumes:
- rustguac-data:/opt/rustguac/data
- rustguac-recordings:/opt/rustguac/recordings
environment:
- RUST_LOG=info
volumes:
rustguac-data:
rustguac-recordings:
```
## Option D: RPM package
+31
View File
@@ -0,0 +1,31 @@
diff --git a/src/protocols/rdp/channels/disp.c b/src/protocols/rdp/channels/disp.c
index 0293843c..a1b2c3d4 100644
--- a/src/protocols/rdp/channels/disp.c
+++ b/src/protocols/rdp/channels/disp.c
@@ -161,6 +161,10 @@ void guac_rdp_disp_load_plugin(rdpContext* context) {
void guac_rdp_disp_set_size(guac_rdp_disp* disp, guac_rdp_settings* settings,
freerdp* rdp_inst, int width, int height) {
+ /* Abort if display module or settings are not yet initialized */
+ if (disp == NULL || settings == NULL)
+ return;
+
guac_rect resize = {
.left = 0,
.top = 0,
diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c
index 06bfac13..c8e1d2f7 100644
--- a/src/protocols/rdp/input.c
+++ b/src/protocols/rdp/input.c
@@ -105,6 +105,11 @@ int guac_rdp_user_size_handler(guac_user* user, int width, int height) {
guac_rdp_settings* settings = rdp_client->settings;
freerdp* rdp_inst = rdp_client->rdp_inst;
+ /* Abort if not yet fully initialized (browser may send size instruction
+ * before the RDP connection is fully established) */
+ if (settings == NULL || rdp_client->disp == NULL)
+ return 0;
+
/* Convert client pixels to remote pixels */
width = width * settings->resolution / user->info.optimal_resolution;
height = height * settings->resolution / user->info.optimal_resolution;
+22 -7
View File
@@ -206,11 +206,17 @@ pub async fn auth_status(
Extension(site_title): Extension<SiteTitle>,
Extension(theme): Extension<ThemeData>,
) -> impl IntoResponse {
let mut resp = json!({ "oidc_enabled": oidc_enabled.0, "site_title": site_title.0 });
if let Some(ref t) = theme.0 {
if let Ok(v) = serde_json::to_value(t) {
resp["theme"] = v;
}
let mut resp = json!({
"oidc_enabled": oidc_enabled.0,
"site_title": site_title.0,
});
resp["theme"] = json!({
"admin_preset": theme.admin_preset,
"admin_colors": theme.admin_colors,
"presets": theme.presets,
});
if let Some(ref url) = theme.logo_url {
resp["theme"]["logo_url"] = json!(url);
}
Json(resp)
}
@@ -219,9 +225,18 @@ pub async fn auth_status(
#[derive(Clone)]
pub struct SiteTitle(pub String);
/// Theme configuration from config, shared via Extension.
/// Resolved theme data shared via Extension.
#[derive(Clone)]
pub struct ThemeData(pub Option<crate::config::ThemeConfig>);
pub struct ThemeData {
/// Admin-configured preset name (e.g. "dark").
pub admin_preset: String,
/// Fully-resolved admin theme colors (preset + overrides).
pub admin_colors: crate::config::ThemeColors,
/// Optional custom logo URL.
pub logo_url: Option<String>,
/// All built-in preset palettes for client-side switching.
pub presets: std::collections::HashMap<String, crate::config::ThemeColors>,
}
/// Marker for whether OIDC is configured.
#[derive(Clone)]
+367
View File
@@ -243,8 +243,281 @@ pub struct Config {
pub recording: Option<RecordingConfig>,
}
/// Fully-resolved theme palette with all 26 color fields.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ThemeColors {
pub primary: String,
pub primary_hover: String,
pub accent: String,
pub accent_hover: String,
pub bg: String,
pub surface: String,
pub input: String,
pub text: String,
pub text_muted: String,
pub border: String,
pub text_dim: String,
pub text_on_primary: String,
pub btn_disabled: String,
pub status_pending: String,
pub status_active: String,
pub status_completed: String,
pub status_error: String,
pub status_expired: String,
pub type_ssh_bg: String,
pub type_ssh_fg: String,
pub type_rdp_bg: String,
pub type_rdp_fg: String,
pub type_vnc_bg: String,
pub type_vnc_fg: String,
pub type_web_bg: String,
pub type_web_fg: String,
pub hop_bg: String,
pub hop_fg: String,
}
/// Returns all 6 built-in theme presets.
pub fn builtin_presets() -> Vec<(&'static str, ThemeColors)> {
vec![
(
"dark",
ThemeColors {
primary: "#e94560".into(),
primary_hover: "#c73652".into(),
accent: "#5bc0be".into(),
accent_hover: "#4aa3a1".into(),
bg: "#1a1a2e".into(),
surface: "#16213e".into(),
input: "#0f3460".into(),
text: "#e0e0e0".into(),
text_muted: "#aaa".into(),
border: "#333".into(),
text_dim: "#666".into(),
text_on_primary: "#fff".into(),
btn_disabled: "#555".into(),
status_pending: "#f0c040".into(),
status_active: "#5bc0be".into(),
status_completed: "#888".into(),
status_error: "#e94560".into(),
status_expired: "#666".into(),
type_ssh_bg: "#1b4332".into(),
type_ssh_fg: "#52b788".into(),
type_rdp_bg: "#3d1f00".into(),
type_rdp_fg: "#f0a050".into(),
type_vnc_bg: "#2d1b4e".into(),
type_vnc_fg: "#b07ff0".into(),
type_web_bg: "#1a1a4e".into(),
type_web_fg: "#7b8ff0".into(),
hop_bg: "#1b4332".into(),
hop_fg: "#52b788".into(),
},
),
(
"light",
ThemeColors {
primary: "#2563eb".into(),
primary_hover: "#1d4ed8".into(),
accent: "#0d9488".into(),
accent_hover: "#0f766e".into(),
bg: "#f8fafc".into(),
surface: "#fff".into(),
input: "#f1f5f9".into(),
text: "#1e293b".into(),
text_muted: "#64748b".into(),
border: "#e2e8f0".into(),
text_dim: "#94a3b8".into(),
text_on_primary: "#fff".into(),
btn_disabled: "#cbd5e1".into(),
status_pending: "#d97706".into(),
status_active: "#0d9488".into(),
status_completed: "#94a3b8".into(),
status_error: "#dc2626".into(),
status_expired: "#cbd5e1".into(),
type_ssh_bg: "#dcfce7".into(),
type_ssh_fg: "#166534".into(),
type_rdp_bg: "#ffedd5".into(),
type_rdp_fg: "#9a3412".into(),
type_vnc_bg: "#f3e8ff".into(),
type_vnc_fg: "#6b21a8".into(),
type_web_bg: "#dbeafe".into(),
type_web_fg: "#1e40af".into(),
hop_bg: "#dcfce7".into(),
hop_fg: "#166534".into(),
},
),
(
"high-contrast",
ThemeColors {
primary: "#ff6b6b".into(),
primary_hover: "#ff4444".into(),
accent: "#00ffcc".into(),
accent_hover: "#00ddaa".into(),
bg: "#000".into(),
surface: "#111".into(),
input: "#1a1a1a".into(),
text: "#fff".into(),
text_muted: "#ccc".into(),
border: "#555".into(),
text_dim: "#999".into(),
text_on_primary: "#000".into(),
btn_disabled: "#444".into(),
status_pending: "#ffdd00".into(),
status_active: "#00ffcc".into(),
status_completed: "#999".into(),
status_error: "#ff4444".into(),
status_expired: "#666".into(),
type_ssh_bg: "#003300".into(),
type_ssh_fg: "#00ff66".into(),
type_rdp_bg: "#332200".into(),
type_rdp_fg: "#ffaa00".into(),
type_vnc_bg: "#220033".into(),
type_vnc_fg: "#cc66ff".into(),
type_web_bg: "#000033".into(),
type_web_fg: "#6699ff".into(),
hop_bg: "#003300".into(),
hop_fg: "#00ff66".into(),
},
),
(
"terminal",
ThemeColors {
primary: "#f59e0b".into(),
primary_hover: "#d97706".into(),
accent: "#22c55e".into(),
accent_hover: "#16a34a".into(),
bg: "#0a0a0a".into(),
surface: "#141414".into(),
input: "#1e1e1e".into(),
text: "#33ff33".into(),
text_muted: "#22aa22".into(),
border: "#2a2a2a".into(),
text_dim: "#186818".into(),
text_on_primary: "#000".into(),
btn_disabled: "#333".into(),
status_pending: "#f59e0b".into(),
status_active: "#33ff33".into(),
status_completed: "#22aa22".into(),
status_error: "#ff3333".into(),
status_expired: "#186818".into(),
type_ssh_bg: "#0a200a".into(),
type_ssh_fg: "#33ff33".into(),
type_rdp_bg: "#201a0a".into(),
type_rdp_fg: "#f59e0b".into(),
type_vnc_bg: "#1a0a20".into(),
type_vnc_fg: "#cc66ff".into(),
type_web_bg: "#0a0a20".into(),
type_web_fg: "#6699ff".into(),
hop_bg: "#0a200a".into(),
hop_fg: "#33ff33".into(),
},
),
(
"nord",
ThemeColors {
primary: "#88c0d0".into(),
primary_hover: "#81a1c1".into(),
accent: "#a3be8c".into(),
accent_hover: "#8fbcbb".into(),
bg: "#2e3440".into(),
surface: "#3b4252".into(),
input: "#434c5e".into(),
text: "#eceff4".into(),
text_muted: "#d8dee9".into(),
border: "#4c566a".into(),
text_dim: "#7b88a1".into(),
text_on_primary: "#2e3440".into(),
btn_disabled: "#4c566a".into(),
status_pending: "#ebcb8b".into(),
status_active: "#a3be8c".into(),
status_completed: "#7b88a1".into(),
status_error: "#bf616a".into(),
status_expired: "#4c566a".into(),
type_ssh_bg: "#384838".into(),
type_ssh_fg: "#a3be8c".into(),
type_rdp_bg: "#483e38".into(),
type_rdp_fg: "#ebcb8b".into(),
type_vnc_bg: "#3e3848".into(),
type_vnc_fg: "#b48ead".into(),
type_web_bg: "#384048".into(),
type_web_fg: "#88c0d0".into(),
hop_bg: "#384838".into(),
hop_fg: "#a3be8c".into(),
},
),
(
"corporate",
ThemeColors {
primary: "#3b82f6".into(),
primary_hover: "#2563eb".into(),
accent: "#f97316".into(),
accent_hover: "#ea580c".into(),
bg: "#0f172a".into(),
surface: "#1e293b".into(),
input: "#334155".into(),
text: "#f1f5f9".into(),
text_muted: "#94a3b8".into(),
border: "#475569".into(),
text_dim: "#64748b".into(),
text_on_primary: "#fff".into(),
btn_disabled: "#475569".into(),
status_pending: "#fbbf24".into(),
status_active: "#34d399".into(),
status_completed: "#64748b".into(),
status_error: "#ef4444".into(),
status_expired: "#475569".into(),
type_ssh_bg: "#14532d".into(),
type_ssh_fg: "#4ade80".into(),
type_rdp_bg: "#431407".into(),
type_rdp_fg: "#fb923c".into(),
type_vnc_bg: "#3b0764".into(),
type_vnc_fg: "#c084fc".into(),
type_web_bg: "#172554".into(),
type_web_fg: "#60a5fa".into(),
hop_bg: "#14532d".into(),
hop_fg: "#4ade80".into(),
},
),
(
"avocado",
ThemeColors {
primary: "#d4883c".into(),
primary_hover: "#b8742f".into(),
accent: "#c5d455".into(),
accent_hover: "#a8b83e".into(),
bg: "#151a0e".into(),
surface: "#1e2414".into(),
input: "#2a321c".into(),
text: "#eef0e0".into(),
text_muted: "#a0a888".into(),
border: "#3a4228".into(),
text_dim: "#5a6240".into(),
text_on_primary: "#151a0e".into(),
btn_disabled: "#3a4228".into(),
status_pending: "#d4883c".into(),
status_active: "#c5d455".into(),
status_completed: "#6a7252".into(),
status_error: "#c0392b".into(),
status_expired: "#3a4228".into(),
type_ssh_bg: "#1e2a14".into(),
type_ssh_fg: "#8cb832".into(),
type_rdp_bg: "#2a2014".into(),
type_rdp_fg: "#d4a050".into(),
type_vnc_bg: "#221e2a".into(),
type_vnc_fg: "#b07ff0".into(),
type_web_bg: "#1a1e2a".into(),
type_web_fg: "#7b8ff0".into(),
hop_bg: "#1e2a14".into(),
hop_fg: "#8cb832".into(),
},
),
]
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ThemeConfig {
/// Built-in preset name: dark, light, high-contrast, terminal, nord, corporate.
#[serde(skip_serializing_if = "Option::is_none")]
pub preset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -266,9 +539,103 @@ pub struct ThemeConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub border_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_dim: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_on_primary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub btn_disabled: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_pending: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_active: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_completed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_expired: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_ssh_bg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_ssh_fg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_rdp_bg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_rdp_fg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_vnc_bg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_vnc_fg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_web_bg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_web_fg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hop_bg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hop_fg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,
}
impl ThemeConfig {
/// Resolve config into a full ThemeColors palette.
/// Starts from the named preset (default: "dark"), then applies overrides.
pub fn resolve(&self) -> (String, ThemeColors) {
let preset_name = self.preset.as_deref().unwrap_or("dark");
let presets = builtin_presets();
let mut colors = presets
.iter()
.find(|(name, _)| *name == preset_name)
.map(|(_, c)| c.clone())
.unwrap_or_else(|| presets[0].1.clone());
macro_rules! apply {
($field:ident, $src:ident) => {
if let Some(ref v) = self.$src {
colors.$field = v.clone();
}
};
($field:ident) => {
if let Some(ref v) = self.$field {
colors.$field = v.clone();
}
};
}
apply!(primary, primary_color);
apply!(primary_hover);
apply!(accent, accent_color);
apply!(accent_hover);
apply!(bg, bg_color);
apply!(surface, surface_color);
apply!(input, input_color);
apply!(text, text_color);
apply!(text_muted);
apply!(border, border_color);
apply!(text_dim);
apply!(text_on_primary);
apply!(btn_disabled);
apply!(status_pending);
apply!(status_active);
apply!(status_completed);
apply!(status_error);
apply!(status_expired);
apply!(type_ssh_bg);
apply!(type_ssh_fg);
apply!(type_rdp_bg);
apply!(type_rdp_fg);
apply!(type_vnc_bg);
apply!(type_vnc_fg);
apply!(type_web_bg);
apply!(type_web_fg);
apply!(hop_bg);
apply!(hop_fg);
(preset_name.to_string(), colors)
}
}
fn default_listen_addr() -> String {
"127.0.0.1:8089".into()
}
+19 -1
View File
@@ -559,7 +559,25 @@ async fn run_server(config: Config, database: Db) {
let oidc_enabled = OidcEnabled(oidc_state.is_some());
let vault_configured = VaultConfigured(config.vault.is_some());
let site_title = SiteTitle(config.site_title.clone());
let theme_data = ThemeData(config.theme.clone());
let theme_data = {
let (admin_preset, admin_colors) = config
.theme
.as_ref()
.map(|t| t.resolve())
.unwrap_or_else(|| ("dark".into(), crate::config::builtin_presets()[0].1.clone()));
let logo_url = config.theme.as_ref().and_then(|t| t.logo_url.clone());
let presets: std::collections::HashMap<String, crate::config::ThemeColors> =
crate::config::builtin_presets()
.into_iter()
.map(|(name, colors)| (name.to_string(), colors))
.collect();
ThemeData {
admin_preset,
admin_colors,
logo_url,
presets,
}
};
let trusted_proxies = auth::TrustedProxies(config.trusted_proxies.clone());
// Periodically clean up expired auth sessions from the database
+16 -3
View File
@@ -274,9 +274,20 @@ impl SessionManager {
created_by: String,
) -> Result<SessionInfo, SessionError> {
let session_id = Uuid::new_v4();
let width = req.width.unwrap_or(1920);
let height = req.height.unwrap_or(1080);
let dpi = req.dpi.unwrap_or(96);
let raw_width = req.width.unwrap_or(1920);
let raw_height = req.height.unwrap_or(1080);
let raw_dpi = req.dpi.unwrap_or(96);
let width = raw_width.clamp(640, 8192);
let height = raw_height.clamp(480, 8192);
let dpi = raw_dpi.clamp(16, 384);
if width != raw_width || height != raw_height || dpi != raw_dpi {
tracing::warn!(
session_id = %session_id,
raw_width, raw_height, raw_dpi,
clamped_width = width, clamped_height = height, clamped_dpi = dpi,
"Clamped session dimensions to safe range"
);
}
let (
mut conn_params,
@@ -377,6 +388,7 @@ impl SessionManager {
session_id = %session_id,
hostname = %hostname,
username = %username,
width, height, dpi,
"Creating new RDP session"
);
@@ -455,6 +467,7 @@ impl SessionManager {
tracing::info!(
session_id = %session_id,
hostname = %hostname,
width, height, dpi,
"Creating new VNC session"
);
+68 -13
View File
@@ -18,10 +18,21 @@ use serde::Deserialize;
use serde_json::json;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Instant;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
/// Which side terminated the proxy connection.
enum ProxyResult {
/// guacd closed the connection (with optional error).
GuacdEnded(Option<String>),
/// Browser/WebSocket closed the connection (with optional error).
BrowserEnded(Option<String>),
/// Session was cancelled externally.
Cancelled,
}
#[derive(Deserialize)]
pub struct WsQuery {
pub token: Option<String>,
@@ -163,12 +174,52 @@ async fn handle_ws(
};
// Run the bidirectional proxy
let result = proxy_ws_guacd(ws, guacd_stream, recording_file, cancel).await;
let start = Instant::now();
let proxy_result = proxy_ws_guacd(ws, guacd_stream, recording_file, cancel).await;
let elapsed = start.elapsed();
manager.disconnect_viewer(session_id).await;
if let Err(e) = result {
tracing::warn!(session_id = %session_id, client_ip = %client_addr, error = %e, "WebSocket proxy ended with error");
// Log termination direction and timing
let mark_error = match &proxy_result {
ProxyResult::GuacdEnded(err) => {
if elapsed.as_secs() < 5 {
tracing::warn!(
session_id = %session_id, client_ip = %client_addr,
elapsed_ms = elapsed.as_millis() as u64,
error = ?err,
"guacd closed connection quickly (possible connection failure)"
);
true // mark as error
} else {
tracing::info!(
session_id = %session_id, client_ip = %client_addr,
elapsed_secs = elapsed.as_secs(),
"Proxy ended: guacd closed connection"
);
false
}
}
ProxyResult::BrowserEnded(err) => {
tracing::info!(
session_id = %session_id, client_ip = %client_addr,
elapsed_secs = elapsed.as_secs(),
error = ?err,
"Proxy ended: browser disconnected"
);
false
}
ProxyResult::Cancelled => {
tracing::info!(
session_id = %session_id, client_ip = %client_addr,
elapsed_secs = elapsed.as_secs(),
"Proxy ended: session cancelled"
);
false
}
};
if mark_error {
manager.error_session(session_id).await;
} else {
// Only mark completed if no more active connections
@@ -203,7 +254,7 @@ async fn proxy_ws_guacd(
guacd: GuacdStream,
recording_file: Option<tokio::fs::File>,
cancel: CancellationToken,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
) -> ProxyResult {
let (guacd_read, guacd_write) = tokio::io::split(guacd);
let (ws_write, ws_read) = ws.split();
@@ -220,21 +271,25 @@ async fn proxy_ws_guacd(
// Wait for either direction to finish, or cancellation
tokio::select! {
result = guacd_to_browser => {
if let Ok(Err(e)) = result {
tracing::debug!("guacd→browser ended: {}", e);
}
let err = match result {
Ok(Err(e)) => Some(e.to_string()),
Err(e) => Some(e.to_string()),
_ => None,
};
ProxyResult::GuacdEnded(err)
}
result = browser_to_guacd => {
if let Ok(Err(e)) = result {
tracing::debug!("browser→guacd ended: {}", e);
}
let err = match result {
Ok(Err(e)) => Some(e.to_string()),
Err(e) => Some(e.to_string()),
_ => None,
};
ProxyResult::BrowserEnded(err)
}
_ = cancel.cancelled() => {
tracing::info!("Session cancelled, shutting down proxy");
ProxyResult::Cancelled
}
}
Ok(())
}
/// Forward data from guacd to WebSocket, recording along the way.
+38 -30
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - Address Book</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; }
h1 { color: var(--primary); }
a { color: var(--accent); }
@@ -12,8 +12,21 @@
nav a { margin-right: 1.5em; text-decoration: none; }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
nav .logout { color: #888; float: right; cursor: pointer; }
nav .logout:hover { color: var(--primary); }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
.layout { display: flex; gap: 2em; }
.sidebar { min-width: 220px; max-width: 280px; }
@@ -32,10 +45,10 @@
.folder-list li:hover { background: var(--surface); }
.folder-list li.selected { background: var(--input); color: var(--accent); }
.folder-name-row { display: flex; align-items: center; gap: 0.5em; flex-wrap: wrap; }
.folder-scope { font-size: 0.7em; color: #666; background: var(--input); padding: 0.1em 0.4em; border-radius: 2px; }
.folder-scope { font-size: 0.7em; color: var(--text-dim); background: var(--input); padding: 0.1em 0.4em; border-radius: 2px; }
.folder-list li.selected .folder-scope { background: var(--surface); }
.folder-desc { font-size: 0.8em; color: #666; display: block; margin-top: 0.15em; }
.folder-count { font-size: 0.7em; color: #555; display: block; margin-top: 0.15em; }
.folder-desc { font-size: 0.8em; color: var(--text-dim); display: block; margin-top: 0.15em; }
.folder-count { font-size: 0.7em; color: var(--text-dim); display: block; margin-top: 0.15em; }
.entries-table { border-collapse: collapse; width: 100%; }
.entries-table th, .entries-table td {
@@ -50,15 +63,15 @@
font-size: 0.85em;
font-weight: bold;
}
.type-ssh { background: #1b4332; color: #52b788; }
.type-rdp { background: #3d1f00; color: #f0a050; }
.type-vnc { background: #2d1b4e; color: #b07ff0; }
.type-web { background: #1a1a4e; color: #7b8ff0; }
.type-ssh { background: var(--type-ssh-bg); color: var(--type-ssh-fg); }
.type-rdp { background: var(--type-rdp-bg); color: var(--type-rdp-fg); }
.type-vnc { background: var(--type-vnc-bg); color: var(--type-vnc-fg); }
.type-web { background: var(--type-web-bg); color: var(--type-web-fg); }
button {
padding: 0.4em 1em;
background: var(--primary);
color: #fff;
color: var(--text-on-primary);
border: none;
font-family: monospace;
font-size: 0.9em;
@@ -66,7 +79,7 @@
cursor: pointer;
}
button:hover { background: var(--primary-hover); }
button:disabled { background: #555; cursor: default; }
button:disabled { background: var(--btn-disabled); cursor: default; }
.btn-connect { background: var(--accent); color: var(--bg); font-weight: bold; }
.btn-connect:hover { background: var(--accent-hover); }
.btn-small {
@@ -77,9 +90,9 @@
.btn-add { background: var(--input); color: var(--accent); border: 1px solid var(--border); }
.btn-add:hover { background: var(--surface); }
.empty { color: #666; margin: 2em 0; }
.empty { color: var(--text-dim); margin: 2em 0; }
#global-error { color: var(--primary); margin-top: 0.8em; white-space: pre-wrap; }
.no-vault { color: #888; margin: 2em 0; }
.no-vault { color: var(--text-muted); margin: 2em 0; }
/* Empty state card */
.empty-state {
@@ -141,7 +154,7 @@
.modal-actions .btn-cancel { background: var(--border); }
.modal-actions .btn-cancel:hover { background: #444; }
.field-hint { font-size: 0.8em; color: #555; margin-top: 0.2em; }
.field-hint { font-size: 0.8em; color: var(--text-dim); margin-top: 0.2em; }
/* Hop cards */
.hop-card {
@@ -226,7 +239,7 @@
white-space: nowrap;
}
.flow-node-you { background: var(--input); color: var(--text-muted); }
.flow-node-hop { background: #1b4332; color: #52b788; }
.flow-node-hop { background: var(--hop-bg); color: var(--hop-fg); }
.flow-node-target { background: var(--input); color: var(--accent); font-weight: bold; }
.flow-arrow { color: var(--text-muted); margin: 0 0.3em; }
</style>
@@ -240,7 +253,7 @@
<a href="/docs.html">Docs</a>
<a href="/tokens.html" id="tokens-link" style="display:none">Tokens</a>
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
<span class="logout" id="logout-btn">logout</span>
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
</nav>
<div id="global-error"></div>
@@ -529,16 +542,15 @@
</div>
<script>
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Address Book';document.querySelector('h1').textContent=d.site_title;}
applyTheme(d.theme);
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
@@ -550,7 +562,7 @@
.catch(function() { window.location.href = '/'; });
}
document.getElementById('logout-btn').addEventListener('click', function() {
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
fetch('/auth/logout', { credentials: 'same-origin' })
.finally(function() { window.location.href = '/'; });
@@ -817,10 +829,6 @@
btn.textContent = 'Connecting...';
clearError();
var body = extraBody || {};
// Send browser dimensions so the session starts at the right resolution
if (!body.width) body.width = window.innerWidth;
if (!body.height) body.height = window.innerHeight;
if (!body.dpi) body.dpi = Math.round((window.devicePixelRatio || 1) * 96);
fetch('/api/addressbook/folders/' + encodeURIComponent(scope) + '/' + encodeURIComponent(folder) + '/entries/' + encodeURIComponent(name) + '/connect', {
method: 'POST',
headers: apiHeaders({ 'Content-Type': 'application/json' }),
+27 -15
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - Admin</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; }
h1 { color: var(--primary); }
h2 { color: var(--primary); font-size: 1em; margin-top: 2em; }
@@ -13,8 +13,21 @@
nav a { margin-right: 1.5em; text-decoration: none; }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
nav .logout { color: #888; float: right; cursor: pointer; }
nav .logout:hover { color: var(--primary); }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
table { border-collapse: collapse; width: 100%; margin-top: 0.5em; }
th, td { text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border); }
@@ -36,7 +49,7 @@
.btn-small:hover { text-decoration: underline; }
.btn-action { color: var(--accent); }
button.btn-primary {
padding: 0.4em 1.2em; background: var(--primary); color: #fff; border: none;
padding: 0.4em 1.2em; background: var(--primary); color: var(--text-on-primary); border: none;
font-family: monospace; font-size: 0.9em; border-radius: 3px; cursor: pointer;
}
button.btn-primary:hover { background: var(--primary-hover); }
@@ -52,7 +65,7 @@
.disabled-row { opacity: 0.5; }
.status-active { color: var(--accent); }
.status-disabled { color: var(--primary); }
.status-expired { color: #666; }
.status-expired { color: var(--status-expired); }
#error { color: var(--primary); margin-top: 0.5em; }
.token-reveal {
background: var(--surface); padding: 1em 1.5em; border-radius: 6px;
@@ -77,7 +90,7 @@
<a href="/docs.html">Docs</a>
<a href="/tokens.html">Tokens</a>
<a href="/admin.html" class="active">Admin</a>
<span class="logout" id="logout-btn">logout</span>
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
</nav>
<h2>Users</h2>
@@ -155,16 +168,15 @@
<div id="error"></div>
<script>
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Admin';document.querySelector('h1').textContent=d.site_title;}
applyTheme(d.theme);
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
@@ -196,7 +208,7 @@
}
checkAdmin();
document.getElementById('logout-btn').addEventListener('click', function() {
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
fetch('/auth/logout', { credentials: 'same-origin' })
.finally(function() { window.location.href = '/'; });
+20 -24
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - SSH Session</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #888; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1a3a2a; --type-ssh-fg: #5bc0be; --type-rdp-bg: #2a1a3a; --type-rdp-fg: #a78bfa; --type-vnc-bg: #3a2a1a; --type-vnc-fg: #f0c040; --type-web-bg: #1a2a3a; --type-web-fg: #60a5fa; --hop-bg: #1e3a5f; --hop-fg: #7ec8e3; }
html, body {
margin: 0;
padding: 0;
@@ -60,7 +60,7 @@
#banner-continue {
padding: 0.6em 2em;
background: var(--primary);
color: #fff;
color: var(--text-on-primary);
border: none;
font-family: monospace;
font-size: 1em;
@@ -116,15 +116,13 @@
<div id="display"></div>
<script>
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);}
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Session';}
applyTheme(d.theme);
initTheme(d.theme);
});
var pathParts = window.location.pathname.split('/');
var sessionId = pathParts[pathParts.length - 1];
@@ -232,7 +230,7 @@
clipboardPanel.innerHTML =
'<div style="display:flex;align-items:center;padding:0.8em 1em;border-bottom:1px solid var(--border);background:var(--surface);">' +
'<h3 style="margin:0;flex:1;color:var(--primary);font-size:1.1em;">Clipboard</h3>' +
'<button id="cp-close" style="background:none;border:none;color:#888;font-size:1.6em;cursor:pointer;font-family:monospace;padding:0 0.3em;">&times;</button>' +
'<button id="cp-close" style="background:none;border:none;color:var(--text-dim);font-size:1.6em;cursor:pointer;font-family:monospace;padding:0 0.3em;">&times;</button>' +
'</div>' +
'<div style="flex:1;display:flex;flex-direction:column;padding:1em;gap:0.8em;overflow-y:auto;min-height:0;">' +
'<div style="color:var(--text-muted);font-size:0.95em;">Shared clipboard. Paste text below and click Send, or copy text received from the remote session.</div>' +
@@ -243,7 +241,7 @@
'<button id="cp-clear" style="padding:0.4em 0.8em;background:var(--border);color:var(--text-muted);border:none;font-family:monospace;font-size:0.95em;border-radius:3px;cursor:pointer;">Clear</button>' +
'</div>' +
'<div id="cp-status" style="color:var(--accent);font-size:0.9em;min-height:1.2em;"></div>' +
'<div style="color:#888;font-size:0.85em;line-height:1.4;">Press <b>Ctrl+Alt+Shift</b> to toggle this panel.<br>Text copied in the remote session appears here automatically.</div>' +
'<div style="color:var(--text-dim);font-size:0.85em;line-height:1.4;">Press <b>Ctrl+Alt+Shift</b> to toggle this panel.<br>Text copied in the remote session appears here automatically.</div>' +
'</div>';
document.body.appendChild(clipboardPanel);
@@ -283,7 +281,7 @@
var toggleTab = document.createElement('div');
toggleTab.textContent = '📋 Clipboard';
toggleTab.title = 'Toggle clipboard panel (Ctrl+Alt+Shift)';
toggleTab.style.cssText = 'position:fixed;left:-1px;top:50%;transform:translateY(-50%);writing-mode:vertical-rl;background:var(--primary);color:#fff;padding:0.6em 0.3em;font-family:monospace;font-size:0.75em;cursor:pointer;z-index:3001;border-radius:0 4px 4px 0;opacity:0.6;letter-spacing:0.1em;';
toggleTab.style.cssText = 'position:fixed;left:-1px;top:50%;transform:translateY(-50%);writing-mode:vertical-rl;background:var(--primary);color:var(--text-on-primary);padding:0.6em 0.3em;font-family:monospace;font-size:0.75em;cursor:pointer;z-index:3001;border-radius:0 4px 4px 0;opacity:0.6;letter-spacing:0.1em;';
toggleTab.addEventListener('mouseenter', function() { toggleTab.style.opacity = '1'; });
toggleTab.addEventListener('mouseleave', function() { toggleTab.style.opacity = '0.6'; });
toggleTab.addEventListener('click', function() { toggleClipboardPanel(); });
@@ -368,7 +366,7 @@
var fileTab = document.createElement('div');
fileTab.textContent = 'Files';
fileTab.title = 'Toggle file manager';
fileTab.style.cssText = 'display:none;position:fixed;left:-1px;top:calc(50% + 60px);transform:translateY(-50%);writing-mode:vertical-rl;background:var(--accent);color:var(--bg);padding:0.6em 0.3em;font-family:monospace;font-size:0.75em;cursor:pointer;z-index:3001;border-radius:0 4px 4px 0;opacity:0.6;letter-spacing:0.1em;font-weight:bold;';
fileTab.style.cssText = 'display:none;position:fixed;left:-1px;top:calc(50% + 60px);transform:translateY(-50%);writing-mode:vertical-rl;background:var(--accent);color:var(--text-on-primary);padding:0.6em 0.3em;font-family:monospace;font-size:0.75em;cursor:pointer;z-index:3001;border-radius:0 4px 4px 0;opacity:0.6;letter-spacing:0.1em;font-weight:bold;';
fileTab.addEventListener('mouseenter', function() { fileTab.style.opacity = '1'; });
fileTab.addEventListener('mouseleave', function() { fileTab.style.opacity = '0.6'; });
fileTab.addEventListener('click', function() { toggleFilePanel(); });
@@ -381,16 +379,16 @@
filePanel.innerHTML =
'<div style="display:flex;align-items:center;padding:0.8em 1em;border-bottom:1px solid var(--border);background:var(--surface);">' +
'<h3 style="margin:0;flex:1;color:var(--accent);font-size:1.1em;">File Manager</h3>' +
'<button id="fp-close" style="background:none;border:none;color:#888;font-size:1.6em;cursor:pointer;font-family:monospace;padding:0 0.3em;">&times;</button>' +
'<button id="fp-close" style="background:none;border:none;color:var(--text-dim);font-size:1.6em;cursor:pointer;font-family:monospace;padding:0 0.3em;">&times;</button>' +
'</div>' +
'<div id="fp-breadcrumb" style="padding:0.5em 1em;color:var(--text-muted);font-size:0.95em;border-bottom:1px solid #222;background:#0f1a2e;word-break:break-all;"></div>' +
'<div style="padding:0.5em 1em;display:flex;gap:0.5em;border-bottom:1px solid #222;">' +
'<div id="fp-breadcrumb" style="padding:0.5em 1em;color:var(--text-muted);font-size:0.95em;border-bottom:1px solid var(--border);background:var(--input);word-break:break-all;"></div>' +
'<div style="padding:0.5em 1em;display:flex;gap:0.5em;border-bottom:1px solid var(--border);">' +
'<button id="fp-upload" style="padding:0.4em 0.8em;background:var(--accent);color:var(--bg);font-weight:bold;border:none;font-family:monospace;font-size:0.95em;border-radius:3px;cursor:pointer;">Upload</button>' +
'<button id="fp-refresh" style="padding:0.4em 0.8em;background:var(--input);color:var(--accent);border:1px solid var(--border);font-family:monospace;font-size:0.95em;border-radius:3px;cursor:pointer;">Refresh</button>' +
'</div>' +
'<div id="fp-list" style="flex:1;overflow-y:auto;min-height:0;"></div>' +
'<div id="fp-status" style="padding:0.5em 1em;color:var(--accent);font-size:0.9em;min-height:1.5em;border-top:1px solid #222;"></div>' +
'<div style="padding:0.6em 1em;color:#888;font-size:0.85em;border-top:1px solid #222;">Drag files here to upload. Files are temporary and will be deleted when the session ends.</div>';
'<div id="fp-status" style="padding:0.5em 1em;color:var(--accent);font-size:0.9em;min-height:1.5em;border-top:1px solid var(--border);"></div>' +
'<div style="padding:0.6em 1em;color:var(--text-dim);font-size:0.85em;border-top:1px solid var(--border);">Drag files here to upload. Files are temporary and will be deleted when the session ends.</div>';
document.body.appendChild(filePanel);
fileListEl = document.getElementById('fp-list');
@@ -522,13 +520,13 @@
return;
}
if (fileListEl) fileListEl.innerHTML = '<div style="padding:1em;color:#888;">Loading...</div>';
if (fileListEl) fileListEl.innerHTML = '<div style="padding:1em;color:var(--text-dim);">Loading...</div>';
fetchDirectoryListing(path);
}
function refreshDirectory() {
delete cachedListings[currentPath];
if (fileListEl) fileListEl.innerHTML = '<div style="padding:1em;color:#888;">Loading...</div>';
if (fileListEl) fileListEl.innerHTML = '<div style="padding:1em;color:var(--text-dim);">Loading...</div>';
fetchDirectoryListing(currentPath);
}
@@ -536,7 +534,7 @@
if (!fileListEl) return;
var entries = Object.keys(listing);
if (entries.length === 0) {
fileListEl.innerHTML = '<div style="padding:1em;color:#888;">Empty directory</div>';
fileListEl.innerHTML = '<div style="padding:1em;color:var(--text-dim);">Empty directory</div>';
return;
}
@@ -570,7 +568,7 @@
files.sort(function(a, b) { return a.displayName.localeCompare(b.displayName); });
if (dirs.length === 0 && files.length === 0) {
fileListEl.innerHTML = '<div style="padding:1em;color:#888;">Empty directory</div>';
fileListEl.innerHTML = '<div style="padding:1em;color:var(--text-dim);">Empty directory</div>';
return;
}
@@ -710,8 +708,6 @@
case Guacamole.Client.State.WAITING: statusEl.textContent = 'Waiting for server...'; statusEl.className = ''; break;
case Guacamole.Client.State.CONNECTED:
statusEl.textContent = 'Connected'; statusEl.className = 'connected';
// Send actual browser dimensions so guacd can resize the remote display
client.sendSize(window.innerWidth, window.innerHeight);
break;
case Guacamole.Client.State.DISCONNECTING: statusEl.textContent = 'Disconnecting...'; statusEl.className = ''; break;
case Guacamole.Client.State.DISCONNECTED: statusEl.textContent = 'Disconnected'; statusEl.className = ''; break;
+25 -13
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - Docs</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
* { box-sizing: border-box; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; margin: 0; }
h1 { color: var(--primary); }
@@ -12,8 +12,21 @@
nav a { margin-right: 1.5em; text-decoration: none; color: var(--accent); }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
nav .logout { color: #888; float: right; cursor: pointer; }
nav .logout:hover { color: var(--primary); }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
/* Layout */
.docs-layout { display: flex; min-height: calc(100vh - 140px); border-top: 1px solid var(--border); margin-top: 1em; }
@@ -107,7 +120,7 @@
<a href="/docs.html" class="active">Docs</a>
<a href="/tokens.html" id="tokens-link" style="display:none">Tokens</a>
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
<span class="logout" id="logout-btn">logout</span>
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
</nav>
<div class="docs-layout">
@@ -183,16 +196,15 @@
});
// Theme and auth
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Docs';document.querySelector('h1').textContent=d.site_title;}
applyTheme(d.theme);
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
var roleLevel = { admin: 4, poweruser: 3, operator: 2, viewer: 1 };
@@ -216,7 +228,7 @@
})
.catch(function() { window.location.href = '/'; });
}
document.getElementById('logout-btn').addEventListener('click', function() {
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
fetch('/auth/logout', { credentials: 'same-origin' })
.finally(function() { window.location.href = '/'; });
+8 -11
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #888; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1a3a2a; --type-ssh-fg: #5bc0be; --type-rdp-bg: #2a1a3a; --type-rdp-fg: #a78bfa; --type-vnc-bg: #3a2a1a; --type-vnc-fg: #f0c040; --type-web-bg: #1a2a3a; --type-web-fg: #60a5fa; --hop-bg: #1e3a5f; --hop-fg: #7ec8e3; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; }
h1 { color: var(--primary); }
#error { color: var(--primary); margin-top: 0.8em; }
@@ -70,7 +70,7 @@
margin-top: 1.2em;
padding: 0.5em 1.5em;
background: var(--primary);
color: #fff;
color: var(--text-on-primary);
border: none;
font-family: monospace;
font-size: 1em;
@@ -78,7 +78,7 @@
cursor: pointer;
}
#login-form button:hover { background: var(--primary-hover); }
#login-form button:disabled { background: #555; cursor: default; }
#login-form button:disabled { background: var(--btn-disabled); cursor: default; }
</style>
</head>
<body>
@@ -122,13 +122,10 @@
if (res.ok) window.location.href = '/addressbook.html';
});
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}}
var loginForm = document.getElementById('login-form');
var apiKeyToggle = document.getElementById('api-key-toggle');
@@ -148,7 +145,7 @@
document.title = data.site_title;
document.querySelector('h1').textContent = data.site_title;
}
applyTheme(data.theme);
initTheme(data.theme);
});
document.getElementById('sso-btn').addEventListener('click', function() {
+6 -1
View File
@@ -3,6 +3,11 @@
<path d="M64 8C40 8 24 28 20 52c-4 24 4 48 20 60 8 6 16 8 24 8s16-2 24-8c16-12 24-36 20-60C104 28 88 8 64 8z" fill="#5bc0be"/>
<!-- Avocado pit / terminal screen -->
<ellipse cx="64" cy="72" rx="26" ry="30" fill="#1a1a2e"/>
<!-- Eyes -->
<circle cx="55" cy="62" r="3" fill="#e0e0e0"/>
<circle cx="73" cy="62" r="3" fill="#e0e0e0"/>
<!-- Smile -->
<path d="M56 72 Q64 80 72 72" fill="none" stroke="#e0e0e0" stroke-width="2" stroke-linecap="round"/>
<!-- Terminal prompt -->
<text x="48" y="80" font-family="monospace" font-weight="bold" font-size="28" fill="#e94560">&gt;_</text>
<text x="48" y="92" font-family="monospace" font-weight="bold" font-size="18" fill="#e94560">&gt;_</text>
</svg>

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 725 B

+26 -14
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - Recordings</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; }
h1 { color: var(--primary); }
a { color: var(--accent); }
@@ -12,8 +12,21 @@
nav a { margin-right: 1.5em; text-decoration: none; }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
nav .logout { color: #888; float: right; cursor: pointer; }
nav .logout:hover { color: var(--primary); }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
table { border-collapse: collapse; width: 100%; max-width: 800px; }
th, td { text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border); }
@@ -24,7 +37,7 @@
}
.btn-small:hover { text-decoration: underline; }
.btn-delete { color: var(--primary); }
.empty { color: #666; margin: 2em 0; }
.empty { color: var(--text-dim); margin: 2em 0; }
#player-section {
display: none;
@@ -153,7 +166,7 @@
<a href="/docs.html">Docs</a>
<a href="/tokens.html" id="tokens-link" style="display:none">Tokens</a>
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
<span class="logout" id="logout-btn">logout</span>
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
</nav>
<strong>Recordings</strong>
@@ -176,16 +189,15 @@
</div>
<script>
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Recordings';document.querySelector('h1').textContent=d.site_title;}
applyTheme(d.theme);
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
@@ -212,7 +224,7 @@
.catch(function() { window.location.href = '/'; });
}
document.getElementById('logout-btn').addEventListener('click', function() {
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
fetch('/auth/logout', { credentials: 'same-origin' })
.finally(function() { window.location.href = '/'; });
+33 -26
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - Sessions</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; }
h1 { color: var(--primary); }
h2 { color: var(--primary); font-size: 1em; margin-top: 2em; }
@@ -14,8 +14,21 @@
nav a { margin-right: 1.5em; text-decoration: none; }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
nav .logout { color: #888; float: right; cursor: pointer; }
nav .logout:hover { color: var(--primary); }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
form {
background: var(--surface);
@@ -43,7 +56,7 @@
margin-top: 1.2em;
padding: 0.5em 1.5em;
background: var(--primary);
color: #fff;
color: var(--text-on-primary);
border: none;
font-family: monospace;
font-size: 1em;
@@ -51,7 +64,7 @@
cursor: pointer;
}
button:hover { background: var(--primary-hover); }
button:disabled { background: #555; cursor: default; }
button:disabled { background: var(--btn-disabled); cursor: default; }
#error { color: var(--primary); margin-top: 0.8em; white-space: pre-wrap; }
#sessions { margin-top: 2em; }
#sessions table { border-collapse: collapse; width: 100%; }
@@ -61,11 +74,11 @@
#sessions th { color: var(--text-muted); font-size: 0.85em; }
#sessions a { text-decoration: none; }
#sessions a:hover { text-decoration: underline; }
.status-pending { color: #f0c040; }
.status-active { color: var(--accent); }
.status-completed { color: #888; }
.status-error { color: var(--primary); }
.status-expired { color: #666; }
.status-pending { color: var(--status-pending); }
.status-active { color: var(--status-active); }
.status-completed { color: var(--status-completed); }
.status-error { color: var(--status-error); }
.status-expired { color: var(--status-expired); }
.btn-small {
background: none; border: none; color: var(--primary);
cursor: pointer; font-family: monospace; padding: 0; font-size: 0.9em;
@@ -156,7 +169,7 @@
white-space: nowrap;
}
.flow-node-you { background: var(--input); color: var(--text-muted); }
.flow-node-hop { background: #1b4332; color: #52b788; }
.flow-node-hop { background: var(--hop-bg); color: var(--hop-fg); }
.flow-node-target { background: var(--input); color: var(--accent); font-weight: bold; }
.flow-arrow { color: var(--text-muted); margin: 0 0.3em; }
@@ -190,7 +203,7 @@
<a href="/docs.html">Docs</a>
<a href="/tokens.html" id="tokens-link" style="display:none">Tokens</a>
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
<span class="logout" id="logout-btn">logout</span>
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
</nav>
<div id="adhoc-notice" style="display:none;background:var(--surface);padding:1em 1.5em;border-radius:6px;max-width:400px;margin:1.5em 0;color:var(--text-muted);">
@@ -303,16 +316,15 @@
</div>
<script>
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Sessions';document.querySelector('h1').textContent=d.site_title;}
applyTheme(d.theme);
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
@@ -353,7 +365,7 @@
checkRole();
}
document.getElementById('logout-btn').addEventListener('click', function() {
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
// Also clear server session cookie via logout endpoint
fetch('/auth/logout', { credentials: 'same-origin' })
@@ -626,11 +638,6 @@
if (hops.length > 0) body.jump_hosts = hops;
var banner = document.getElementById('banner').value;
if (banner) body.banner = banner;
// Send browser dimensions so the session starts at the right resolution
body.width = window.innerWidth;
body.height = window.innerHeight;
body.dpi = Math.round((window.devicePixelRatio || 1) * 96);
fetch('/api/sessions', {
method: 'POST',
headers: apiHeaders({ 'Content-Type': 'application/json' }),
+26 -14
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>rustguac - API Tokens</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; }
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; }
h1 { color: var(--primary); }
h2 { color: var(--primary); font-size: 1em; margin-top: 2em; }
@@ -13,8 +13,21 @@
nav a { margin-right: 1.5em; text-decoration: none; }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
nav .logout { color: #888; float: right; cursor: pointer; }
nav .logout:hover { color: var(--primary); }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
table { border-collapse: collapse; width: 100%; margin-top: 0.5em; }
th, td { text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border); }
@@ -26,7 +39,7 @@
.btn-small:hover { text-decoration: underline; }
.btn-action { color: var(--accent); }
button.btn-primary {
padding: 0.4em 1.2em; background: var(--primary); color: #fff; border: none;
padding: 0.4em 1.2em; background: var(--primary); color: var(--text-on-primary); border: none;
font-family: monospace; font-size: 0.9em; border-radius: 3px; cursor: pointer;
}
button.btn-primary:hover { background: var(--primary-hover); }
@@ -75,7 +88,7 @@
<a href="/docs.html">Docs</a>
<a href="/tokens.html" class="active">Tokens</a>
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
<span class="logout" id="logout-btn">logout</span>
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
</nav>
<div id="loading">Loading...</div>
@@ -118,16 +131,15 @@
<div id="error"></div>
<script>
function applyTheme(theme) {
if (!theme) return;
var props = {primary_color:'--primary',primary_hover:'--primary-hover',accent_color:'--accent',accent_hover:'--accent-hover',bg_color:'--bg',surface_color:'--surface',input_color:'--input',text_color:'--text',text_muted:'--text-muted',border_color:'--border'};
var r = document.documentElement.style;
for (var k in props) { if (theme[k]) r.setProperty(props[k], theme[k]); }
if (theme.logo_url) { var logo = document.getElementById('site-logo'); if (logo) { logo.src = theme.logo_url; logo.style.display = ''; } }
}
(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - API Tokens';document.querySelector('h1').textContent=d.site_title;}
applyTheme(d.theme);
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
@@ -145,7 +157,7 @@
var errorEl = document.getElementById('error');
function showError(msg) { errorEl.textContent = msg; setTimeout(function(){ errorEl.textContent = ''; }, 5000); }
document.getElementById('logout-btn').addEventListener('click', function() {
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
fetch('/auth/logout', { credentials: 'same-origin' })
.finally(function() { window.location.href = '/'; });