diff --git a/src_assets/common/assets/web/Apps.vue b/src_assets/common/assets/web/Apps.vue
index 8aaa73db252..c34fca0d017 100644
--- a/src_assets/common/assets/web/Apps.vue
+++ b/src_assets/common/assets/web/Apps.vue
@@ -49,7 +49,7 @@
![]()
{
globalThis.location.replace(logoutPageUrl.toString())
}
- request.open('GET', '/', true, 'sunshine-logout', cacheBuster)
+ request.open('GET', './', true, 'sunshine-logout', cacheBuster)
request.setRequestHeader('Cache-Control', 'no-store')
request.onload = finish
request.onerror = finish
diff --git a/src_assets/common/assets/web/Troubleshooting.vue b/src_assets/common/assets/web/Troubleshooting.vue
index b1ae7ba0b7e..93174f005c1 100644
--- a/src_assets/common/assets/web/Troubleshooting.vue
+++ b/src_assets/common/assets/web/Troubleshooting.vue
@@ -805,7 +805,7 @@
},
created() {
this._logsCopyTimeout = null;
- fetch("/api/config")
+ fetch("./api/config")
.then((r) => r.json())
.then((r) => {
this.platform = r.platform;
@@ -1003,7 +1003,7 @@
},
ddResetPersistence() {
this.ddResetPressed = true;
- apiFetch("/api/reset-display-device-persistence", {
+ apiFetch("./api/reset-display-device-persistence", {
method: "POST",
headers: {
"Content-Type": "application/json"
@@ -1084,7 +1084,7 @@
});
},
refreshVirtualInputStatus() {
- fetch("/api/virtual-input/status")
+ fetch("./api/virtual-input/status")
.then((r) => r.json())
.then((r) => {
const virtualhid = r.virtualhid || {};
@@ -1216,7 +1216,7 @@
refreshLicenseStatus() {
this.licenseBusy = true;
this.licenseError = '';
- fetch("/api/virtual-input/license")
+ fetch("./api/virtual-input/license")
.then(async (response) => {
const status = await response.json();
if (!response.ok) {
@@ -1243,7 +1243,7 @@
body.license_key = this.licenseKey.trim();
}
- apiFetch("/api/virtual-input/license", {
+ apiFetch("./api/virtual-input/license", {
method: "POST",
headers: {
"Content-Type": "application/json",
diff --git a/src_assets/common/assets/web/index.html b/src_assets/common/assets/web/index.html
index 71f8bd9abbf..b4d6a3caf0d 100644
--- a/src_assets/common/assets/web/index.html
+++ b/src_assets/common/assets/web/index.html
@@ -4,7 +4,24 @@
-
+
Sunshine
diff --git a/src_assets/common/assets/web/router.js b/src_assets/common/assets/web/router.js
index 96873f24fb6..2c36a7de692 100644
--- a/src_assets/common/assets/web/router.js
+++ b/src_assets/common/assets/web/router.js
@@ -6,7 +6,7 @@ import { createRouter, createWebHistory } from 'vue-router'
* Lazy page imports keep the source split into focused components while the
* server serves the same index document for every browser route.
*/
-const routes = [
+export const routes = [
{ path: '/', component: () => import('./Home.vue') },
{ path: '/apps', component: () => import('./Apps.vue') },
{ path: '/clients', redirect: '/' },
diff --git a/tests/web/Troubleshooting.test.js b/tests/web/Troubleshooting.test.js
index 98f0ff2c4c8..c9c14794b9c 100644
--- a/tests/web/Troubleshooting.test.js
+++ b/tests/web/Troubleshooting.test.js
@@ -21,10 +21,10 @@ import en from '../../src_assets/common/assets/web/public/assets/locale/en.json'
*/
async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {}, permissions = [], i18n = null) {
vi.stubGlobal('fetch', vi.fn(async (url) => {
- if (url === '/api/config') {
+ if (url === './api/config') {
return { json: async () => ({ platform, gamepad_driver: gamepadDriver }) }
}
- if (url === '/api/virtual-input/status') {
+ if (url === './api/virtual-input/status') {
return {
json: async () => ({
virtualhid: {
@@ -37,7 +37,7 @@ async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {},
}),
}
}
- if (url === '/api/virtual-input/license') {
+ if (url === './api/virtual-input/license') {
return { ok: true, json: async () => ({ service_available: true, state: 'licensed', licensed: true, ...licenseStatus }) }
}
if (url === './api/permissions') {
@@ -243,7 +243,7 @@ describe('virtual input troubleshooting', () => {
expect(status.attributes('title')).toBe('troubleshooting.driver_status_compatible')
expect(status.attributes('role')).toBeUndefined()
expect(status.attributes('tabindex')).toBeUndefined()
- expect(fetch).toHaveBeenCalledWith('/api/virtual-input/status')
+ expect(fetch).toHaveBeenCalledWith('./api/virtual-input/status')
wrapper.unmount()
})
diff --git a/tests/web/base_path.test.js b/tests/web/base_path.test.js
new file mode 100644
index 00000000000..84982f78848
--- /dev/null
+++ b/tests/web/base_path.test.js
@@ -0,0 +1,91 @@
+import { readdirSync, readFileSync } from 'node:fs'
+import { dirname, join, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+
+import { routes } from '../../src_assets/common/assets/web/router.js'
+
+const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../src_assets/common/assets/web')
+const indexHtml = readFileSync(join(webRoot, 'index.html'), 'utf8')
+
+/**
+ * @brief Extract the classic inline script that resolves the base path from index.html.
+ *
+ * @return {string} Inline script source.
+ */
+function baseScriptSource() {
+ const match = /