diff --git a/functions/router/index.js b/functions/router/index.js deleted file mode 100644 index 54f4f7a2..00000000 --- a/functions/router/index.js +++ /dev/null @@ -1,28 +0,0 @@ -export default async function(req) { - const url = new URL(req.url); - const path = url.pathname; - - // Determine version from path - let version = 'latest'; - if (path.startsWith('/v/')) { - const match = path.match(/^\/v\/([^\/]+)/); - if (match) version = match[1]; - } - - // For HTML navigation (no file extension), serve index.html - if (!path.match(/\.[a-z]+$/i)) { - const htmlUrl = `https://${url.host}/v/${version}/index.html`; - const response = await fetch(htmlUrl); - return new Response(response.body, { - headers: { - 'Content-Type': 'text/html', - 'Cache-Control': 'no-cache' - } - }); - } - - // For assets, pass through - const assetUrl = `https://${url.host}${path}`; - return fetch(assetUrl); -} - diff --git a/functions/router/package.json b/functions/router/package.json deleted file mode 100644 index a2a59768..00000000 --- a/functions/router/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "router", - "version": "1.0.0", - "main": "index.js" -} - diff --git a/scripts/deploy_cloud_app.sh b/scripts/deploy_cloud_app.sh new file mode 100755 index 00000000..08e3c6a9 --- /dev/null +++ b/scripts/deploy_cloud_app.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +# Get current branch name +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Verify branch name matches release/x.x.x or release/x.x.x-dev... +if [[ ! $CURRENT_BRANCH =~ ^release/[0-9]+\.[0-9]+\.[0-9]+(-dev[0-9]+)?$ ]]; then + echo "✗ Error: Current branch '$CURRENT_BRANCH' does not match required pattern" + echo " Expected: release/x.x.x OR release/x.x.x-dev20241104123632" + exit 1 +fi + +# Extract version from branch name (remove "release/" prefix) +VERSION=${CURRENT_BRANCH#release/} + +echo "Current branch: $CURRENT_BRANCH" +echo "Version: $VERSION" +echo "" + +# Change to ui directory +cd ui + +# Ask for confirmation +read -p "Do you want to deploy the cloud app to production? (y/N): " -n 1 -r +echo "" +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Deployment cancelled." + exit 0 +fi + +# Build for root dist +echo "" +echo "Building for root dist..." +npm ci +npm run build:prod + +# Build for versioned dist/v/VERSION +echo "" +echo "Building for dist/v/${VERSION}..." +npm ci +npm run build:prod -- --base=/v/${VERSION}/ --outDir dist/v/${VERSION} + +# Deploy to production +echo "" +echo "Deploying to r2://jetkvm-cloud-app..." +rclone copyto dist r2://jetkvm-cloud-app + +echo "" +echo "✓ Successfully deployed v${VERSION} to production" diff --git a/ui/build-all.sh b/ui/build-all.sh deleted file mode 100755 index 1ed908c8..00000000 --- a/ui/build-all.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e - -# Build latest (includes device list) -echo "Building latest..." -VITE_BASE_PATH=/v/latest/ npm run build:prod -- --outDir dist-temp -mkdir -p dist/v/latest -cp -r dist-temp/* dist/v/latest/ - -# Build for each firmware version you support -echo "Building v2025.11.07..." -VITE_BASE_PATH=/v/2025.11.07/ npm run build:prod -- --outDir dist-temp -mkdir -p dist/v/2025.11.07 -cp -r dist-temp/* dist/v/2025.11.07/ - -echo "Building v2025.10.15..." -VITE_BASE_PATH=/v/2025.10.15/ npm run build:prod -- --outDir dist-temp -mkdir -p dist/v/2025.10.15 -cp -r dist-temp/* dist/v/2025.10.15/ - -rm -rf dist-temp -echo "✓ All versions built to dist/v/" - diff --git a/ui/package-lock.json b/ui/package-lock.json index bd1d54ef..c4e1c96c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -36,6 +36,7 @@ "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^3.3.0", + "semver": "^7.7.3", "tailwind-merge": "^3.3.1", "tslog": "^4.10.2", "usehooks-ts": "^3.1.1", @@ -6823,7 +6824,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/ui/package.json b/ui/package.json index f56ff63a..fc4ad519 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,14 +4,13 @@ "version": "2025.11.07.2130", "type": "module", "engines": { - "node": "22.x" + "node": "^22.20.0" }, "scripts": { "dev": "./dev_device.sh", "dev:ssl": "USE_SSL=true ./dev_device.sh", "dev:cloud": "vite dev --mode=cloud-development", "build": "npm run build:prod", - "build:all": "./build-all.sh", "build:device": "npm run i18n:compile && tsc && vite build --mode=device --emptyOutDir", "build:staging": "npm run i18n:compile && tsc && vite build --mode=cloud-staging", "build:prod": "npm run i18n:compile && tsc && vite build --mode=cloud-production", @@ -56,6 +55,7 @@ "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^3.3.0", + "semver": "^7.7.3", "tailwind-merge": "^3.3.1", "tslog": "^4.10.2", "usehooks-ts": "^3.1.1", diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 6ae358f2..04f536f3 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -211,8 +211,8 @@ export const Button = React.forwardRef( Button.displayName = "Button"; -type LinkPropsType = Pick & - React.ComponentProps & { disabled?: boolean, reloadDocument?: boolean }; +type LinkPropsType = Pick & + React.ComponentProps & { disabled?: boolean }; export const LinkButton = ({ to, ...props }: LinkPropsType) => { const classes = cx( "group outline-hidden", @@ -224,7 +224,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => { if (to.toString().startsWith("http")) { return ( - + ); diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index c44fea38..a0571c6b 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -2,6 +2,8 @@ import { Link } from "react-router"; import { MdConnectWithoutContact } from "react-icons/md"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { LuEllipsisVertical } from "react-icons/lu"; +import semver from "semver"; +import { useMemo } from "react"; import Card from "@components/Card"; import { Button, LinkButton } from "@components/Button"; @@ -45,12 +47,33 @@ export default function KvmCard({ id, online, lastSeen, + appVersion, }: { title: string; id: string; online: boolean; lastSeen: Date | null; + appVersion: string; }) { + /** + * Constructs the URL for connecting to this KVM device's interface. + * + * Version 0.4.91 is the last backwards-compatible UI that works with older devices. + * Devices on v0.4.91 or below are served that version, while newer devices get + * their actual version. Unparseable versions fall back to 0.4.91 for safety. + */ + const kvmUrl = useMemo(() => { + const BACKWARDS_COMPATIBLE_VERSION = "0.4.91"; + + // Use device version if valid and >= 0.4.91, otherwise fall back to backwards-compatible version + const shouldUseDeviceVersion = + semver.valid(appVersion) && semver.gte(appVersion, BACKWARDS_COMPATIBLE_VERSION); + const version = shouldUseDeviceVersion ? appVersion : BACKWARDS_COMPATIBLE_VERSION; + + return new URL(`/v/${version}/devices/${id}`, window.location.origin).toString(); + }, [appVersion, id]); + + return (
@@ -89,7 +112,9 @@ export default function KvmCard({ text={m.connect_to_kvm()} LeadingIcon={MdConnectWithoutContact} textAlign="center" - to={`/devices/${id}`} + reloadDocument + target="_self" + to={kvmUrl} /> ) : (