Compare commits

...
5 Commits
Author SHA1 Message Date
mrhid6 c3c58581cc fix: Fixed scale and mouse handler
Server Deploy / deploy (push) Successful in 1m38s
2026-07-20 09:49:20 +01:00
mrhid6 c558b81471 fix: Fixed keyboard disconnect
Server Deploy / deploy (push) Successful in 41s
2026-07-20 09:42:07 +01:00
mrhid6 963fa9c877 fix: Fixed mouse position on console
Server Deploy / deploy (push) Successful in 39s
2026-07-20 09:36:57 +01:00
mrhid6 a02747d02e fix: Fixed console resolution
Server Deploy / deploy (push) Successful in 44s
2026-07-20 09:30:44 +01:00
mrhid6 db5b5e173f fix: Ci and agent update
Server Deploy / deploy (push) Successful in 13s
Agent Release / build (push) Successful in 10m32s
Agent Release / msi (push) Successful in 36s
2026-07-17 16:43:57 +01:00
6 changed files with 101 additions and 31 deletions
-16
View File
@@ -86,22 +86,6 @@ jobs:
$env:GOOS = "windows"; $env:GOARCH = "amd64"
go build -ldflags="-s -w -X main.Version=$env:VERSION" -o ../installer/vantage-agent-windows-amd64.exe ./cmd
- name: Cache nssm
id: cache-nssm
uses: actions/cache@v4
with:
path: installer/nssm.exe
key: nssm-2.24-win64
- name: Fetch nssm
if: steps.cache-nssm.outputs.cache-hit != 'true'
working-directory: installer
shell: pwsh
run: |
Invoke-WebRequest -Uri https://nssm.cc/release/nssm-2.24.zip -OutFile nssm.zip
Expand-Archive -Path nssm.zip -DestinationPath nssm-extract -Force
Copy-Item nssm-extract/nssm-2.24/win64/nssm.exe -Destination nssm.exe
- name: Install WiX
shell: pwsh
run: dotnet tool install --global wix --version 5.*
+1 -1
View File
@@ -4,7 +4,7 @@ build
.env
docs
.superpowers
installer/*.exe
installer/vantage-agent-windows-amd64.exe
installer/*.msi
installer/nssm.zip
installer/checksums-msi.txt
Binary file not shown.
+8 -2
View File
@@ -32,6 +32,10 @@ function Invoke-Native {
function Invoke-NativeSoft {
param([string]$File, [string[]]$Arguments)
Write-Log ("RUN(soft): {0} {1}" -f $File, ($Arguments -join " "))
# Native stderr merged via 2>&1 becomes terminating errors under
# ErrorActionPreference=Stop; force Continue in this scope so a benign nssm
# message (e.g. "service has not been started") never aborts setup.
$ErrorActionPreference = "Continue"
$out = & $File @Arguments 2>&1
if ($out) { Write-Log ("OUT: {0}" -f ($out -join "`n")) }
Write-Log ("EXIT: {0}" -f $LASTEXITCODE)
@@ -121,8 +125,10 @@ tls: true
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppRotateOnline", "1")
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppRotateBytes", "1048576")
# restart (not just start) so an upgrade picks up the new binary
Invoke-NativeSoft -File $nssm -Arguments @("restart", "VantageAgent")
# Service is freshly (re)installed and stopped here (teardown removed the old
# one on upgrade), so start it. "restart" would try to stop a not-running
# service and emit a stderr error.
Invoke-NativeSoft -File $nssm -Arguments @("start", "VantageAgent")
Write-Log "=== setup ok ==="
exit 0
+44 -6
View File
@@ -15,7 +15,11 @@ export default function ServerConsolePage() {
const serverId = params.id as string;
const containerRef = useRef<HTMLDivElement>(null);
const connectionRef = useRef<{ disconnect: () => void } | null>(null);
const connectionRef = useRef<{
disconnect: () => void;
setScale: (scale: number) => void;
resize: (width: number, height: number) => void;
} | null>(null);
const [protocol, setProtocol] = useState<string>(searchParams.get("protocol") || "");
const [keyId, setKeyId] = useState<string>("");
@@ -27,6 +31,8 @@ export default function ServerConsolePage() {
const [connected, setConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState<{ token: string; wsPath: string } | null>(null);
const [zoom, setZoom] = useState(1);
const dprRef = useRef(1);
// Inject the vendored Guacamole client script once.
useEffect(() => {
@@ -103,17 +109,36 @@ export default function ServerConsolePage() {
const wsProto = location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${wsProto}://${location.host}${pending.wsPath}`;
const rect = containerRef.current.getBoundingClientRect();
const dpi = Math.round(96 * (window.devicePixelRatio || 1));
const dpr = window.devicePixelRatio || 1;
dprRef.current = dpr;
// Request the remote at device-pixel resolution with a fixed 96 dpi, then
// scale the display back down by dpr. Folding dpr into `dpi` instead makes
// the remote enlarge everything, which reads as a zoomed-in view.
const connectData =
`token=${encodeURIComponent(pending.token)}` +
`&width=${Math.floor(rect.width)}` +
`&height=${Math.floor(rect.height)}` +
`&dpi=${dpi}`;
`&width=${Math.floor(rect.width * dpr)}` +
`&height=${Math.floor(rect.height * dpr)}` +
`&dpi=96`;
connectionRef.current = openConsole(containerRef.current, wsUrl, connectData);
connectionRef.current.setScale(zoom / dpr);
setPending(null);
}, [connected, pending]);
// Apply zoom live without reconnecting: resize the remote to a resolution
// that, once scaled to fit the container, yields the requested zoom. Higher
// zoom = fewer remote pixels rendered larger. Display always fits the
// container exactly, so no scrollbars appear.
useEffect(() => {
if (!connectionRef.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const dpr = dprRef.current;
const remoteW = Math.floor((rect.width * dpr) / zoom);
const remoteH = Math.floor((rect.height * dpr) / zoom);
connectionRef.current.resize(remoteW, remoteH);
connectionRef.current.setScale(zoom / dpr);
}, [zoom]);
function handleDisconnect() {
connectionRef.current?.disconnect();
connectionRef.current = null;
@@ -246,12 +271,25 @@ export default function ServerConsolePage() {
<Button variant="danger" onClick={handleDisconnect}>
Disconnect
</Button>
<label className="text-sm text-text-secondary">Scale</label>
<select
value={zoom}
onChange={(e) => setZoom(Number(e.target.value))}
className="rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
>
<option value={0.5}>50%</option>
<option value={0.75}>75%</option>
<option value={1}>100%</option>
<option value={1.25}>125%</option>
<option value={1.5}>150%</option>
<option value={2}>200%</option>
</select>
</div>
)}
<div
ref={containerRef}
className="min-h-[500px] flex-1 rounded-lg border border-border bg-black"
className="min-h-[500px] flex-1 overflow-hidden rounded-lg border border-border bg-black"
/>
</div>
);
+48 -6
View File
@@ -2,7 +2,11 @@
// The library attaches a global `Guacamole` object when loaded.
declare const Guacamole: any;
export function openConsole(container: HTMLElement, wsUrl: string, connectData = ""): { disconnect: () => void } {
export function openConsole(
container: HTMLElement,
wsUrl: string,
connectData = ""
): { disconnect: () => void; setScale: (scale: number) => void; resize: (width: number, height: number) => void } {
// Guacamole's WebSocketTunnel builds the socket URL as `wsUrl + "?" + data`,
// so wsUrl must NOT already contain a query string — pass params via connectData.
const tunnel = new Guacamole.WebSocketTunnel(wsUrl);
@@ -10,20 +14,58 @@ export function openConsole(container: HTMLElement, wsUrl: string, connectData =
container.innerHTML = "";
container.appendChild(client.getDisplay().getElement());
// Make the console focusable so keyboard capture is scoped to it (see below).
container.tabIndex = 0;
client.connect(connectData);
// Wire keyboard + mouse.
const mouse = new Guacamole.Mouse(client.getDisplay().getElement());
mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = (state: any) =>
client.sendMouseState(state);
const keyboard = new Guacamole.Keyboard(document);
const display = client.getDisplay();
let scale = 1;
// Wire keyboard + mouse. The display element is rendered at `scale` of the
// remote's native resolution, but Guacamole.Mouse reports coordinates in
// element (on-screen) pixels. Divide by scale to map back to remote
// coordinates, otherwise the cursor is offset.
const mouse = new Guacamole.Mouse(display.getElement());
mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = (state: any) => {
const s = new Guacamole.Mouse.State(
state.x / scale,
state.y / scale,
state.left,
state.middle,
state.right,
state.up,
state.down
);
client.sendMouseState(s);
};
// Scope keyboard capture to the container rather than `document`, so it only
// grabs keys while the console is focused and stops entirely once the element
// is removed (navigating away / disconnect). Attaching to `document` leaks the
// capture and swallows keystrokes in unrelated inputs.
const keyboard = new Guacamole.Keyboard(container);
keyboard.onkeydown = (k: number) => client.sendKeyEvent(1, k);
keyboard.onkeyup = (k: number) => client.sendKeyEvent(0, k);
// Guacamole.Mouse consumes the native mousedown, so clicking the console never
// moves DOM focus back to it. Refocus explicitly so keyboard capture resumes.
const refocus = () => container.focus();
container.addEventListener("mousedown", refocus);
container.focus();
return {
disconnect() {
container.removeEventListener("mousedown", refocus);
keyboard.onkeydown = null;
keyboard.onkeyup = null;
if (typeof keyboard.reset === "function") keyboard.reset();
client.disconnect();
},
setScale(s: number) {
scale = s;
display.scale(s);
},
resize(width: number, height: number) {
client.sendSize(width, height);
},
};
}