From 7b09ac7f80b21c0dd5bf971d38066937a7818637 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 18 May 2026 00:03:49 +0200 Subject: [PATCH 01/22] redirect old url's --- src/hooks.client.ts | 2 + src/lib/utils/legacy-hash-redirect.ts | 78 +++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/lib/utils/legacy-hash-redirect.ts diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 96b5ce6d..ed4431ed 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -4,6 +4,7 @@ import { authState, startAuthLifecycle } from '$lib/state/auth-state.svelte'; import { backendMetadata } from '$lib/state/backend-metadata-state.svelte'; import { initializeColorScheme } from '$lib/state/color-scheme-state.svelte'; import { userState } from '$lib/state/user-state.svelte'; +import { redirectLegacyHashRoute } from '$lib/utils/legacy-hash-redirect'; async function clientInit(): Promise { const { data } = await metaApi.versionGetBackendInfo(); @@ -20,6 +21,7 @@ async function clientInit(): Promise { } export async function init() { + redirectLegacyHashRoute(); await clientInit().catch(handleApiError); initializeColorScheme(); } diff --git a/src/lib/utils/legacy-hash-redirect.ts b/src/lib/utils/legacy-hash-redirect.ts new file mode 100644 index 00000000..4f6061f2 --- /dev/null +++ b/src/lib/utils/legacy-hash-redirect.ts @@ -0,0 +1,78 @@ +const GUID = '([0-9a-fA-F-]{36})'; +const TOKEN = '([^/]+)'; + +const EXACT: Record = { + '/': '/home', + '/dashboard': '/home', + '/dashboard/home': '/home', + '/dashboard/shockers': '/shockers/own', + '/dashboard/shockers/own': '/shockers/own', + '/dashboard/shockers/shared': '/shockers/shared', + '/dashboard/shares': '/shares/public', + '/dashboard/shares/links': '/shares/public', + '/dashboard/admin': '/admin/online-hubs', + '/dashboard/admin/users': '/admin/users', + '/dashboard/admin/online-devices': '/admin/online-hubs', + '/dashboard/profile': '/settings/account', + '/dashboard/profile/account': '/settings/account', + '/dashboard/profile/settings': '/settings/account', + '/dashboard/profile/license': '/settings/account', + '/dashboard/profile/connections': '/settings/connections', + '/dashboard/profile/connections/patreon': '/settings/connections', + '/dashboard/devices': '/hubs', + '/dashboard/tokens': '/settings/api-tokens', + '/account': '/login', + '/account/login': '/login', + '/account/signup': '/signup', + '/account/password': '/forgot-password', + '/account/password/reset': '/forgot-password', + '/public': '/', + '/public/home': '/', + '/public/proxy/token': '/t', +}; + +const PATTERNS: { re: RegExp; to: string }[] = [ + { re: new RegExp(`^/dashboard/shockers/${GUID}/shares$`), to: '/shockers/$1/edit' }, + { re: new RegExp(`^/dashboard/shockers/${GUID}/logs$`), to: '/shockers/logs/$1' }, + { re: new RegExp(`^/dashboard/shares/links/${GUID}$`), to: '/shares/public/$1/edit' }, + { re: new RegExp(`^/dashboard/devices/${GUID}/setup$`), to: '/hubs' }, + { re: new RegExp(`^/dashboard/devices/${GUID}/ota$`), to: '/hubs/$1/update' }, + { re: new RegExp(`^/account/password/recover/${TOKEN}/${TOKEN}$`), to: '/forgot-password' }, + { re: new RegExp(`^/account/activate/${TOKEN}/${TOKEN}$`), to: '/activate' }, + { re: new RegExp(`^/public/shares/links/${GUID}$`), to: '/shares/public/$1' }, + { re: new RegExp(`^/public/proxy/shares/links/${GUID}$`), to: '/shares/public/$1' }, + { re: new RegExp(`^/public/proxy/shares/code/${GUID}$`), to: '/usc/$1' }, +]; + +/** + * Maps a legacy WebUI hash route (without the leading `#`) to its new path, + * or returns `null` if the path doesn't look like a legacy route. + */ +export function mapLegacyHashRoute(legacyPath: string): string | null { + if (!legacyPath.startsWith('/')) return null; + + const qIdx = legacyPath.indexOf('?'); + const path = qIdx === -1 ? legacyPath : legacyPath.slice(0, qIdx); + const search = qIdx === -1 ? '' : legacyPath.slice(qIdx); + + if (Object.prototype.hasOwnProperty.call(EXACT, path)) return EXACT[path] + search; + + for (const { re, to } of PATTERNS) { + if (re.test(path)) return path.replace(re, to) + search; + } + + // Unknown legacy route → home so the user isn't dumped on a 404. + return '/home' + search; +} + +/** + * If the current URL has a legacy `#/...` hash route, replace it with the + * mapped new path. Call before the SvelteKit router boots. + */ +export function redirectLegacyHashRoute(): void { + const hash = location.hash; + if (!hash || hash.charAt(1) !== '/') return; + + const target = mapLegacyHashRoute(hash.slice(1)); + if (target) location.replace(target); +} From 3af64814b58784bde47567a772971d47f5a5dd53 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 18 May 2026 00:04:47 +0200 Subject: [PATCH 02/22] welcome screen --- src/hooks.client.ts | 2 + src/lib/state/expiring-flags.ts | 87 ++++++++++++++ src/routes/+layout.svelte | 3 + src/routes/WelcomeScreen.svelte | 205 ++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 src/lib/state/expiring-flags.ts create mode 100644 src/routes/WelcomeScreen.svelte diff --git a/src/hooks.client.ts b/src/hooks.client.ts index ed4431ed..dd52d63d 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -3,6 +3,7 @@ import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { authState, startAuthLifecycle } from '$lib/state/auth-state.svelte'; import { backendMetadata } from '$lib/state/backend-metadata-state.svelte'; import { initializeColorScheme } from '$lib/state/color-scheme-state.svelte'; +import { expiringFlags } from '$lib/state/expiring-flags'; import { userState } from '$lib/state/user-state.svelte'; import { redirectLegacyHashRoute } from '$lib/utils/legacy-hash-redirect'; @@ -22,6 +23,7 @@ async function clientInit(): Promise { export async function init() { redirectLegacyHashRoute(); + expiringFlags.clearExpired(); await clientInit().catch(handleApiError); initializeColorScheme(); } diff --git a/src/lib/state/expiring-flags.ts b/src/lib/state/expiring-flags.ts new file mode 100644 index 00000000..018239b9 --- /dev/null +++ b/src/lib/state/expiring-flags.ts @@ -0,0 +1,87 @@ +/** + * Tiny localStorage-backed key/value store where each entry has an absolute + * expiry. Entries past their expiry read as missing and are pruned in bulk on + * startup via {@link clearExpired}. + * + * Stored under a single localStorage key so prune/read/write are one + * serialize round-trip and entries can't drift apart on tab close. + */ + +const STORAGE_KEY = 'os.expiringFlags'; + +interface Entry { + /** Arbitrary JSON-serialisable value. */ + v: unknown; + /** Expiry, ms since epoch. */ + e: number; +} + +type Store = Record; + +function read(): Store { + if (typeof localStorage === 'undefined') return {}; + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return {}; + try { + const parsed: unknown = JSON.parse(raw); + if (parsed && typeof parsed === 'object') return parsed as Store; + } catch { + // Corrupt blob; reset. + } + return {}; +} + +function write(store: Store): void { + if (typeof localStorage === 'undefined') return; + if (Object.keys(store).length === 0) { + localStorage.removeItem(STORAGE_KEY); + } else { + localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); + } +} + +function toMs(expiresAt: Date | number): number { + return typeof expiresAt === 'number' ? expiresAt : expiresAt.getTime(); +} + +export const expiringFlags = { + /** + * Returns the stored value for `key`, or `null` if the entry is missing or + * has expired. Expired entries are NOT pruned here (see {@link clearExpired}). + */ + get(key: string): T | null { + const entry = read()[key]; + if (!entry) return null; + if (Date.now() >= entry.e) return null; + return entry.v as T; + }, + + /** Stores `value` under `key` until `expiresAt`. */ + set(key: string, value: unknown, expiresAt: Date | number): void { + const store = read(); + store[key] = { v: value, e: toMs(expiresAt) }; + write(store); + }, + + /** Removes `key`, regardless of expiry. */ + delete(key: string): void { + const store = read(); + if (!(key in store)) return; + delete store[key]; + write(store); + }, + + /** Drops every entry whose expiry has passed. Call once on client startup. */ + clearExpired(): void { + const store = read(); + const now = Date.now(); + let changed = false; + for (const k of Object.keys(store)) { + if (now >= store[k].e) { + delete store[k]; + changed = true; + } + } + if (changed) write(store); + }, +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1780ec24..2906169d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -9,6 +9,7 @@ import Footer from './Footer.svelte'; import Header from './Header.svelte'; import Sidebar from './Sidebar.svelte'; + import WelcomeScreen from './WelcomeScreen.svelte'; import '../app.css'; import { IsMobile } from '$lib/hooks/is-mobile.svelte'; import { usePersistedState } from '$lib/state/classes/persisted-state.svelte'; @@ -36,6 +37,8 @@ + + (sidebarOpen.value = v)}>
diff --git a/src/routes/WelcomeScreen.svelte b/src/routes/WelcomeScreen.svelte new file mode 100644 index 00000000..35a2fcd3 --- /dev/null +++ b/src/routes/WelcomeScreen.svelte @@ -0,0 +1,205 @@ + + + + + + +{#if open} + +{/if} + +{#snippet stepWelcome()} +
+

+ Welcome to the new +

+ + OpenShock + +

+ frontend +

+ +

+ Over 2 years of work. Faster, fully mobile-friendly, and a much better base for everything + we want to build next. +

+

+ Take the quick tour, or skip ahead and dive in. +

+
+{/snippet} + +{#snippet stepFeatures()} +
    +
  • + Reworked shocker dashboard, with groundwork for + customizable layouts. +
  • +
  • + Web Terminal for configuring your hub and flashing + firmware straight from the browser. No installs. +
  • +
  • + OAuth sign-in so you can log in with accounts you + already have. +
  • +
  • + Better sharing: public links, user shares, invite + tracking, and per-shocker shortcuts. +
  • +
+{/snippet} + +{#snippet stepRedirects()} +

+ Old bookmarks like + openshock.app/#/dashboard/home + forward automatically. Nothing for you to do. +

+

+ The legacy site stays online during the transition. We'll announce a sunset date once the new + site has full feature parity. +

+{/snippet} + +{#snippet stepFeedback()} +

+ Something broken or missing? Tell us on Discord or open an issue on GitHub. The faster you + report it, the faster we can fix it. +

+

+ Lots more is landing soon. Thanks for sticking with us. +

+{/snippet} From 2bd90ba3ca6dbb48e25ec78ff899abab6021b435 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 18 May 2026 00:29:38 +0200 Subject: [PATCH 03/22] Update WelcomeScreen.svelte --- src/routes/WelcomeScreen.svelte | 251 ++++++++++++++++++++------------ 1 file changed, 157 insertions(+), 94 deletions(-) diff --git a/src/routes/WelcomeScreen.svelte b/src/routes/WelcomeScreen.svelte index 35a2fcd3..3add7b46 100644 --- a/src/routes/WelcomeScreen.svelte +++ b/src/routes/WelcomeScreen.svelte @@ -17,114 +17,161 @@ {#if open}