mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 17:23:05 +02:00
Support relative URLs. Closes #165.
This commit is contained in:
+12
-12
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#111419" />
|
||||
<meta name="description" content="Web interface for MeshCore mesh radio networks. Send and receive messages, manage contacts and channels, and configure your radio." />
|
||||
<title>RemoteTerm for MeshCore</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="./favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="shortcut icon" href="./favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png" />
|
||||
<link rel="manifest" href="./site.webmanifest" />
|
||||
<script>
|
||||
// Start critical data fetches before React/Vite JS loads.
|
||||
// Must be in <head> BEFORE the module script so the browser queues these
|
||||
@@ -42,17 +42,17 @@
|
||||
});
|
||||
};
|
||||
window.__prefetch = {
|
||||
config: fetchJsonOrThrow('/api/radio/config'),
|
||||
settings: fetchJsonOrThrow('/api/settings'),
|
||||
channels: fetchJsonOrThrow('/api/channels'),
|
||||
contacts: fetchJsonOrThrow('/api/contacts?limit=1000&offset=0'),
|
||||
unreads: fetchJsonOrThrow('/api/read-state/unreads'),
|
||||
undecryptedCount: fetchJsonOrThrow('/api/packets/undecrypted/count'),
|
||||
config: fetchJsonOrThrow('./api/radio/config'),
|
||||
settings: fetchJsonOrThrow('./api/settings'),
|
||||
channels: fetchJsonOrThrow('./api/channels'),
|
||||
contacts: fetchJsonOrThrow('./api/contacts?limit=1000&offset=0'),
|
||||
unreads: fetchJsonOrThrow('./api/read-state/unreads'),
|
||||
undecryptedCount: fetchJsonOrThrow('./api/packets/undecrypted/count'),
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ import type {
|
||||
UnreadCounts,
|
||||
} from './types';
|
||||
|
||||
const API_BASE = '/api';
|
||||
const API_BASE = './api';
|
||||
|
||||
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const hasBody = options?.body !== undefined;
|
||||
|
||||
@@ -131,7 +131,7 @@ export function SettingsAboutSection({
|
||||
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="/api/debug"
|
||||
href="./api/debug"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-muted-foreground hover:text-primary hover:underline"
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Message } from '../types';
|
||||
import { getStateKey } from '../utils/conversationState';
|
||||
|
||||
const STORAGE_KEY = 'meshcore_browser_notifications_enabled_by_conversation';
|
||||
const NOTIFICATION_ICON_PATH = '/favicon-256x256.png';
|
||||
const NOTIFICATION_ICON_PATH = './favicon-256x256.png';
|
||||
|
||||
type NotificationPermissionState = NotificationPermission | 'unsupported';
|
||||
type ConversationNotificationMap = Record<string, boolean>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getStateKey } from '../utils/conversationState';
|
||||
|
||||
const APP_TITLE = 'RemoteTerm for MeshCore';
|
||||
const UNREAD_APP_TITLE = 'RemoteTerm';
|
||||
const BASE_FAVICON_PATH = '/favicon.svg';
|
||||
const BASE_FAVICON_PATH = './favicon.svg';
|
||||
const GREEN_BADGE_FILL = '#16a34a';
|
||||
const RED_BADGE_FILL = '#dc2626';
|
||||
const BADGE_CENTER = 750;
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/contacts?limit=100&offset=0');
|
||||
expect(url).toBe('./api/contacts?limit=100&offset=0');
|
||||
});
|
||||
|
||||
it('builds repeater advert path endpoint query', async () => {
|
||||
@@ -118,7 +118,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.getRepeaterAdvertPaths(12);
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/contacts/repeaters/advert-paths?limit_per_repeater=12');
|
||||
expect(url).toBe('./api/contacts/repeaters/advert-paths?limit_per_repeater=12');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,7 +238,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.sendDirectMessage('abc123', 'hello');
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/messages/direct');
|
||||
expect(url).toBe('./api/messages/direct');
|
||||
expect(options.method).toBe('POST');
|
||||
expect(JSON.parse(options.body)).toEqual({
|
||||
destination: 'abc123',
|
||||
@@ -256,7 +256,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.updateRadioConfig({ name: 'NewName' });
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/radio/config');
|
||||
expect(url).toBe('./api/radio/config');
|
||||
expect(options.method).toBe('PATCH');
|
||||
expect(JSON.parse(options.body)).toEqual({ name: 'NewName' });
|
||||
});
|
||||
@@ -271,7 +271,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.setPrivateKey('my-secret-key');
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/radio/private-key');
|
||||
expect(url).toBe('./api/radio/private-key');
|
||||
expect(options.method).toBe('PUT');
|
||||
expect(JSON.parse(options.body)).toEqual({ private_key: 'my-secret-key' });
|
||||
});
|
||||
@@ -286,7 +286,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.discoverMesh('repeaters');
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/radio/discover');
|
||||
expect(url).toBe('./api/radio/discover');
|
||||
expect(options.method).toBe('POST');
|
||||
expect(JSON.parse(options.body)).toEqual({ target: 'repeaters' });
|
||||
});
|
||||
@@ -301,7 +301,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.deleteContact('pubkey123');
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/contacts/pubkey123');
|
||||
expect(url).toBe('./api/contacts/pubkey123');
|
||||
expect(options.method).toBe('DELETE');
|
||||
});
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.sendAdvertisement();
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/radio/advertise');
|
||||
expect(url).toBe('./api/radio/advertise');
|
||||
expect(options.method).toBe('POST');
|
||||
expect(options.body).toBe(JSON.stringify({ mode: 'flood' }));
|
||||
});
|
||||
@@ -330,7 +330,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.sendAdvertisement('zero_hop');
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/radio/advertise');
|
||||
expect(url).toBe('./api/radio/advertise');
|
||||
expect(options.method).toBe('POST');
|
||||
expect(options.body).toBe(JSON.stringify({ mode: 'zero_hop' }));
|
||||
});
|
||||
@@ -383,7 +383,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
});
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toContain('/api/messages?');
|
||||
expect(url).toContain('./api/messages?');
|
||||
expect(url).toContain('limit=50');
|
||||
expect(url).toContain('offset=10');
|
||||
expect(url).toContain('type=PRIV');
|
||||
@@ -402,7 +402,7 @@ describe('fetchJson (via api methods)', () => {
|
||||
await api.getMessages();
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('/api/messages');
|
||||
expect(url).toBe('./api/messages');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('SettingsAboutSection', () => {
|
||||
);
|
||||
|
||||
const link = screen.getByRole('link', { name: /Open debug support snapshot/i });
|
||||
expect(link).toHaveAttribute('href', '/api/debug');
|
||||
expect(link).toHaveAttribute('href', './api/debug');
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -750,7 +750,7 @@ describe('SettingsModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Statistics/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledWith('/api/statistics', expect.any(Object));
|
||||
expect(fetchSpy).toHaveBeenCalledWith('./api/statistics', expect.any(Object));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('useBrowserNotifications', () => {
|
||||
);
|
||||
expect(window.Notification).toHaveBeenCalledWith('New message in #flightless', {
|
||||
body: 'Notifications will look like this. These require the tab to stay open, and will not be reliable on mobile.',
|
||||
icon: '/favicon-256x256.png',
|
||||
icon: './favicon-256x256.png',
|
||||
tag: `meshcore-notification-preview-channel-${incomingChannelMessage.conversation_key}`,
|
||||
});
|
||||
expect(mocks.toast.warning).toHaveBeenCalledWith('Notifications enabled with warning', {
|
||||
@@ -122,7 +122,7 @@ describe('useBrowserNotifications', () => {
|
||||
expect(window.Notification).toHaveBeenCalledTimes(2);
|
||||
expect(window.Notification).toHaveBeenNthCalledWith(2, 'New message in #flightless', {
|
||||
body: 'hello room',
|
||||
icon: '/favicon-256x256.png',
|
||||
icon: './favicon-256x256.png',
|
||||
tag: 'meshcore-message-42',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,8 +172,8 @@ describe('useFaviconBadge', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getIconHref('icon')).toBe('/favicon.svg');
|
||||
expect(getIconHref('shortcut icon')).toBe('/favicon.svg');
|
||||
expect(getIconHref('icon')).toBe('./favicon.svg');
|
||||
expect(getIconHref('shortcut icon')).toBe('./favicon.svg');
|
||||
});
|
||||
|
||||
rerender({
|
||||
@@ -209,8 +209,8 @@ describe('useFaviconBadge', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getIconHref('icon')).toBe('/favicon.svg');
|
||||
expect(getIconHref('shortcut icon')).toBe('/favicon.svg');
|
||||
expect(getIconHref('icon')).toBe('./favicon.svg');
|
||||
expect(getIconHref('shortcut icon')).toBe('./favicon.svg');
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -54,7 +54,9 @@ export function useWebSocket(options: UseWebSocketOptions) {
|
||||
const connect = useCallback(() => {
|
||||
// Determine WebSocket URL based on current location
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/ws`;
|
||||
// Resolve relative to the page so sub-path reverse proxies work
|
||||
const base = new URL('./api/ws', window.location.href);
|
||||
const wsUrl = `${protocol}//${base.host}${base.pathname}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user