mirror of https://github.com/jetkvm/kvm.git
versioned
This commit is contained in:
parent
7b92ba6354
commit
ba869cd6c2
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "router",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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/"
|
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
|
@ -6823,7 +6824,6 @@
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,13 @@
|
||||||
"version": "2025.11.07.2130",
|
"version": "2025.11.07.2130",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.x"
|
"node": "^22.20.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./dev_device.sh",
|
"dev": "./dev_device.sh",
|
||||||
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
||||||
"dev:cloud": "vite dev --mode=cloud-development",
|
"dev:cloud": "vite dev --mode=cloud-development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:all": "./build-all.sh",
|
|
||||||
"build:device": "npm run i18n:compile && tsc && vite build --mode=device --emptyOutDir",
|
"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:staging": "npm run i18n:compile && tsc && vite build --mode=cloud-staging",
|
||||||
"build:prod": "npm run i18n:compile && tsc && vite build --mode=cloud-production",
|
"build:prod": "npm run i18n:compile && tsc && vite build --mode=cloud-production",
|
||||||
|
|
@ -56,6 +55,7 @@
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
|
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
type LinkPropsType = Pick<LinkProps, "to"> &
|
type LinkPropsType = Pick<LinkProps, "to" | "target" | "reloadDocument"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-hidden",
|
"group outline-hidden",
|
||||||
|
|
@ -224,7 +224,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
|
|
||||||
if (to.toString().startsWith("http")) {
|
if (to.toString().startsWith("http")) {
|
||||||
return (
|
return (
|
||||||
<ExtLink href={to.toString()} className={classes}>
|
<ExtLink href={to.toString()} className={classes} target={props.target}>
|
||||||
<ButtonContent {...props} />
|
<ButtonContent {...props} />
|
||||||
</ExtLink>
|
</ExtLink>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { Link } from "react-router";
|
||||||
import { MdConnectWithoutContact } from "react-icons/md";
|
import { MdConnectWithoutContact } from "react-icons/md";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { LuEllipsisVertical } from "react-icons/lu";
|
import { LuEllipsisVertical } from "react-icons/lu";
|
||||||
|
import semver from "semver";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
@ -45,12 +47,33 @@ export default function KvmCard({
|
||||||
id,
|
id,
|
||||||
online,
|
online,
|
||||||
lastSeen,
|
lastSeen,
|
||||||
|
appVersion,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
id: string;
|
id: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
lastSeen: Date | null;
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="px-5 py-5 space-y-3">
|
<div className="px-5 py-5 space-y-3">
|
||||||
|
|
@ -89,7 +112,9 @@ export default function KvmCard({
|
||||||
text={m.connect_to_kvm()}
|
text={m.connect_to_kvm()}
|
||||||
LeadingIcon={MdConnectWithoutContact}
|
LeadingIcon={MdConnectWithoutContact}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
to={`/devices/${id}`}
|
reloadDocument
|
||||||
|
target="_self"
|
||||||
|
to={kvmUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,8 @@ if (isOnDevice) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
// We only need to do this for the cloud deployment, because the device doesn't need to be versioned
|
||||||
|
], { basename: import.meta.env.BASE_URL });
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,13 @@ import { CLOUD_API } from "@/ui.config";
|
||||||
import { m } from "@localizations/messages";
|
import { m } from "@localizations/messages";
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
devices: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
online: boolean;
|
||||||
|
lastSeen: string;
|
||||||
|
version: string;
|
||||||
|
}[];
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
const loader: LoaderFunction = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
|
|
@ -88,6 +94,7 @@ export default function DevicesRoute() {
|
||||||
title={x.name ?? x.id}
|
title={x.name ?? x.id}
|
||||||
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
||||||
online={x.online}
|
online={x.online}
|
||||||
|
appVersion={x.version}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue