diff --git a/CHANGELOG.md b/CHANGELOG.md index 0376430d..ebd5756b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to Airwave are documented here. +## [0.15.66] - 2026-10-13 + +Fixes the Apple TV dropping back to server setup after a restart (issue #64). + +### Fixed +- **Apple TV now keeps its server and session across a reboot.** On tvOS, the server URL, token, and session were stored in AsyncStorage, which the OS keeps in a purgeable cache and clears when storage runs low — so after a restart the Apple TV had forgotten its server and returned to onboarding. Those small values now live in UserDefaults (via React Native's built-in `Settings`), the one tvOS store with a durability guarantee, so they survive a reboot. No new native module or dependency; iPad, iPhone, and Android are unchanged (they keep the Keychain/Keystore and AsyncStorage). A one-time migration carries over any values that haven't been purged yet; an Apple TV whose cache was already cleared re-onboards once. + +### Internal +- `session-storage.ts` backs the Apple-TV durable store with core RN `Settings` (NSUserDefaults), gated on `Platform.OS === "ios" && Platform.isTV`, using an empty-string tombstone for deletes (UserDefaults has no remove). + ## [0.15.65] - 2026-10-12 ### Fixed diff --git a/apps/desktop-setup/package.json b/apps/desktop-setup/package.json index ffdaa7e9..1db8cf9b 100644 --- a/apps/desktop-setup/package.json +++ b/apps/desktop-setup/package.json @@ -1,6 +1,6 @@ { "name": "@airwave/desktop-setup", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "description": "Airwave Desktop first-run onboarding / settings UI — a small Vite + React app built with @airwave/ui (shadcn + oklch theme), rendered in the desktop app's native webview and driven by the supervisor's /config, /save, /status endpoints.", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 38824544..24e7a715 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "desktop", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "description": "Airwave Desktop — an Electrobun tray-only supervisor that runs embedded Postgres + the Airwave server + admin + tv-web on local ports, next to Plex. The browser is the UI. See .plans/desktop-server.md.", diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 6d4e3ba3..746dc880 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@airwave/mcp", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "description": "MCP server exposing a running Airwave server's tools to any MCP-capable agent.", diff --git a/apps/server/package.json b/apps/server/package.json index e33e59f6..0c319f4b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.15.65", + "version": "0.15.66", "type": "module", "main": "src/index.ts", "scripts": { diff --git a/apps/site/package.json b/apps/site/package.json index a949ddf8..56d60c11 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -1,6 +1,6 @@ { "name": "site", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "scripts": { diff --git a/apps/tv-native/app.json b/apps/tv-native/app.json index fe1ae64d..21c93a09 100644 --- a/apps/tv-native/app.json +++ b/apps/tv-native/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Airwave", "slug": "airwave", - "version": "0.15.65", + "version": "0.15.66", "scheme": "airwave", "orientation": "landscape", "userInterfaceStyle": "dark", diff --git a/apps/tv-native/package.json b/apps/tv-native/package.json index 6d2eda8f..c029699b 100644 --- a/apps/tv-native/package.json +++ b/apps/tv-native/package.json @@ -1,6 +1,6 @@ { "name": "tv-native", - "version": "0.15.65", + "version": "0.15.66", "private": true, "main": "expo-router/entry", "scripts": { diff --git a/apps/tv-native/src/lib/auth.ts b/apps/tv-native/src/lib/auth.ts index e4e9ed81..1aca5786 100644 --- a/apps/tv-native/src/lib/auth.ts +++ b/apps/tv-native/src/lib/auth.ts @@ -1,6 +1,5 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; - -import { CredStore, hydrateSyncCreds } from "./cred-store"; +import { CredStore, clearSyncCreds, hydrateSyncCreds } from "./cred-store"; +import { SessionStorage } from "./session-storage"; /** * Native equivalent of tv-web's `auth-client` + `server-url`, minus better-auth's React client: @@ -8,8 +7,8 @@ import { CredStore, hydrateSyncCreds } from "./cred-store"; * (a) where the server is and (b) the token. * * - The **bearer token** is a credential → `CredStore` (Keychain/Keystore via SecureStore, except on the - * Apple TV where it falls back to AsyncStorage — see `cred-store.ts` for the tvOS release-crash reason). - * - The **server URL** and other prefs are not secret → `AsyncStorage`. + * Apple TV where it uses UserDefaults — see `cred-store.ts` for the tvOS release-crash reason). + * - The **server URL** is not secret → `SessionStorage` (UserDefaults on tvOS, AsyncStorage elsewhere). * * These are read once at startup into module-level values (mirroring tv-web, where `SERVER_URL` is * evaluated at module load and the app re-initialises after onboarding). `loadSession()` hydrates @@ -29,9 +28,9 @@ let token: string | null = null; /** Hydrate the in-memory session from storage. Call once before rendering the app. */ export async function loadSession(): Promise { const [storedUrl, storedToken] = await Promise.all([ - AsyncStorage.getItem(SERVER_KEY), + SessionStorage.getItem(SERVER_KEY), CredStore.getItemAsync(TOKEN_KEY), - // Warm the Apple-TV sync credential cache (for better-auth's expoClient) before any auth screen renders. + // Migrate the Apple-TV better-auth credentials into the durable store before any auth screen reads them. hydrateSyncCreds(), ]); if (storedUrl) serverUrl = storedUrl.replace(/\/+$/, ""); @@ -45,21 +44,27 @@ export function hasServerUrl(): boolean { return serverUrl !== ""; } export async function setServerUrl(raw: string): Promise { - serverUrl = normalizeServerUrl(raw); - await AsyncStorage.setItem(SERVER_KEY, serverUrl); + const next = normalizeServerUrl(raw); + await SessionStorage.setItem(SERVER_KEY, next); + serverUrl = next; } export async function clearServerUrl(): Promise { + await SessionStorage.removeItem(SERVER_KEY); serverUrl = BAKED; - await AsyncStorage.removeItem(SERVER_KEY); } export function getToken(): string | null { return token; } export async function setToken(next: string | null): Promise { + if (next) { + await CredStore.setItemAsync(TOKEN_KEY, next); + } else { + // Sign-out: clear better-auth's cookie/session alongside the bearer token so no stale session lingers. + await clearSyncCreds(); + await CredStore.deleteItemAsync(TOKEN_KEY); + } token = next; - if (next) await CredStore.setItemAsync(TOKEN_KEY, next); - else await CredStore.deleteItemAsync(TOKEN_KEY); } /** Coerce user input into a base URL (add a scheme if missing, drop a trailing slash) — tv-web parity. */ diff --git a/apps/tv-native/src/lib/cred-store.ts b/apps/tv-native/src/lib/cred-store.ts index e06db99b..02a67257 100644 --- a/apps/tv-native/src/lib/cred-store.ts +++ b/apps/tv-native/src/lib/cred-store.ts @@ -1,48 +1,15 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; import * as SecureStore from "expo-secure-store"; -import { Platform } from "react-native"; -/** - * Credential storage with a SecureStore-shaped async interface (`getItemAsync` / `setItemAsync` / - * `deleteItemAsync`), so it's a drop-in for `expo-secure-store` (used by our token store AND by - * `@better-auth/expo`'s `expoClient`). - * - * ## Why this exists — the Apple TV release-build crash - * On the **Apple TV** we deliberately DON'T use `expo-secure-store`. tvOS's keychain is heavily restricted, - * and reading it (`searchKeyChain`) at startup throws — which, on a **release + New-Architecture (bridgeless)** - * build, ABORTS the app before the UI ever appears: - * `*** Terminating app due to uncaught exception 'RCTFatalException: non-std C++ exception'` - * This is React Native framework bug **facebook/react-native#54859** (iOS/tvOS 26 only, release only, fine in - * debug): a throwing async **void** TurboModule method isn't caught in `ObjCTurboModule::performVoidMethodInvocation`, - * so the re-thrown exception on a background queue kills the process. `expo-secure-store` is one of the named - * triggers, and our `loadSession()` hits it at launch — matching the crash exactly. - * - * tvOS keychain persistence is unreliable anyway, so on Apple TV we back credentials with **AsyncStorage**. - * iPad/iOS and Android keep the real Keychain/Keystore via SecureStore (unaffected — this is a no-op there). - * - * NOTE: the `[cred-store]` console logs below fire on the Apple TV only, so you can SEE the keychain-free path - * being exercised in the Metro console (dev client). They log storage KEYS and hit/miss — never token values. - */ -const isAppleTV = Platform.OS === "ios" && Platform.isTV; - -// eslint-disable-next-line no-console -const log = (msg: string) => console.log(`[cred-store] ${msg}`); +import { isAppleTV, SessionStorage, TVSessionStorage } from "./session-storage"; +/** Keep the Apple TV keychain-free: SecureStore access previously crashed release builds + * with RN's New Architecture (facebook/react-native#54859). UserDefaults replaces the + * purgeable AsyncStorage fallback; iOS/iPadOS and Android retain SecureStore. */ export const CredStore = isAppleTV ? { - getItemAsync: async (key: string) => { - const v = await AsyncStorage.getItem(key); - log(`(AppleTV·AsyncStorage) getItemAsync "${key}" → ${v == null ? "miss" : "hit"}`); - return v; - }, - setItemAsync: (key: string, value: string) => { - log(`(AppleTV·AsyncStorage) setItemAsync "${key}"`); - return AsyncStorage.setItem(key, value); - }, - deleteItemAsync: (key: string) => { - log(`(AppleTV·AsyncStorage) deleteItemAsync "${key}"`); - return AsyncStorage.removeItem(key); - }, + getItemAsync: (key: string) => SessionStorage.getItem(key), + setItemAsync: (key: string, value: string) => SessionStorage.setItem(key, value), + deleteItemAsync: (key: string) => SessionStorage.removeItem(key), } : { getItemAsync: (key: string) => SecureStore.getItemAsync(key), @@ -50,42 +17,28 @@ export const CredStore = isAppleTV deleteItemAsync: (key: string) => SecureStore.deleteItemAsync(key), }; -// ── Synchronous credential store for @better-auth/expo's expoClient ────────────────────────────────────── -// better-auth-expo reads its storage SYNCHRONOUSLY (`getItem(key): string | null`) — used by the device-code -// login (`login.tsx`) and `useSession()` (`settings/user.tsx`). On iPad/Android that's expo-secure-store's -// sync accessors (real Keychain/Keystore). On the Apple TV we must NOT touch the keychain at all (same -// RN #54859 crash — logging in or opening Settings → User would hit it), and AsyncStorage has no sync API — -// so we back it with an in-memory cache, hydrated once from AsyncStorage at startup (`hydrateSyncCreds()`, -// awaited in `loadSession()` before any auth screen renders) and write-through to AsyncStorage so the -// better-auth session persists across launches. -const syncCache = new Map(); +// @better-auth/expo 1.6 stores its session under these two keys (storagePrefix "cg"). We migrate only +// those, rather than copying every device preference into credential storage. +const SYNC_KEYS = ["cg_cookie", "cg_session_data"]; -/** Populate the Apple-TV sync cache from AsyncStorage. No-op elsewhere. Awaited at startup in loadSession. */ +/** Trigger the one-time AsyncStorage→UserDefaults migration of the better-auth session at startup, so the + * synchronous reads below see it. Best-effort: UserDefaults reads don't fail, and a missing value just + * means "signed out". No-op off Apple TV. */ export async function hydrateSyncCreds(): Promise { if (!isAppleTV) return; - try { - const keys = await AsyncStorage.getAllKeys(); - const entries = await AsyncStorage.multiGet(keys); - for (const [k, v] of entries) if (v != null) syncCache.set(k, v); - log(`(AppleTV) hydrated SyncCredStore cache: ${syncCache.size} key(s) [${[...syncCache.keys()].join(", ")}]`); - } catch (e) { - log(`(AppleTV) hydrateSyncCreds FAILED: ${String(e)}`); - } + await Promise.allSettled(SYNC_KEYS.map((key) => SessionStorage.getItem(key))); +} + +/** Clear the better-auth session (cookie + session data) on sign-out. No-op off Apple TV. */ +export async function clearSyncCreds(): Promise { + if (!isAppleTV) return; + await Promise.allSettled(SYNC_KEYS.map((key) => CredStore.deleteItemAsync(key))); } +/** The Expo auth client reads its session SYNCHRONOUSLY. On Apple TV both credential interfaces share the + * same durable UserDefaults store (no stale in-memory cache); elsewhere it's SecureStore's sync accessors. */ export const SyncCredStore = isAppleTV - ? { - getItem: (key: string): string | null => { - const v = syncCache.get(key) ?? null; - log(`(AppleTV) SyncCredStore.getItem "${key}" → ${v == null ? "miss" : "hit"}`); - return v; - }, - setItem: (key: string, value: string) => { - log(`(AppleTV) SyncCredStore.setItem "${key}"`); - syncCache.set(key, value); - void AsyncStorage.setItem(key, value); - }, - } + ? TVSessionStorage : { getItem: (key: string) => SecureStore.getItem(key), setItem: (key: string, value: string) => SecureStore.setItem(key, value), diff --git a/apps/tv-native/src/lib/device.ts b/apps/tv-native/src/lib/device.ts index 56622c3c..40925846 100644 --- a/apps/tv-native/src/lib/device.ts +++ b/apps/tv-native/src/lib/device.ts @@ -1,4 +1,4 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; +import { SessionStorage } from "./session-storage"; import { mpvDisplay } from "@airwave/mpv-player"; import { Dimensions, Platform, PixelRatio } from "react-native"; @@ -8,8 +8,8 @@ import { getServerUrl } from "./auth"; * Per-device identity + the capability-diagnostic "done" flag, ported from tv-web's `device.ts`. * The profile is measured device-side but stored in the SERVER's DB per `deviceId`, and the done * flag is keyed by SERVER URL so the diagnostic re-runs on a server switch (a new server has no - * profile for this device). Storage is AsyncStorage; values are hydrated once at startup into - * module vars so the getters are sync (mirroring tv-web, where localStorage is sync). + * profile for this device). Storage is UserDefaults on tvOS, AsyncStorage elsewhere. Values are + * hydrated once at startup into module vars so getters are sync (mirroring tv-web). */ const DEVICE_ID_KEY = "cg-device-id"; const CAPS_DONE_KEY = "cg-caps-done"; @@ -23,12 +23,12 @@ function gen(): string { /** Hydrate device identity + caps-done. Call once at startup (alongside loadSession). */ export async function loadDevice(): Promise { - _deviceId = (await AsyncStorage.getItem(DEVICE_ID_KEY)) ?? ""; + _deviceId = (await SessionStorage.getItem(DEVICE_ID_KEY)) ?? ""; if (!_deviceId) { _deviceId = gen(); - await AsyncStorage.setItem(DEVICE_ID_KEY, _deviceId); + await SessionStorage.setItem(DEVICE_ID_KEY, _deviceId); } - _capsDoneServer = (await AsyncStorage.getItem(CAPS_DONE_KEY)) ?? ""; + _capsDoneServer = (await SessionStorage.getItem(CAPS_DONE_KEY)) ?? ""; } export function deviceId(): string { @@ -41,8 +41,9 @@ export function capsDoneForCurrentServer(): boolean { } export async function markCapsDone(): Promise { - _capsDoneServer = getServerUrl(); - await AsyncStorage.setItem(CAPS_DONE_KEY, _capsDoneServer); + const server = getServerUrl(); + await SessionStorage.setItem(CAPS_DONE_KEY, server); + _capsDoneServer = server; } /** The device report sent on sign-in. On native the codec facts come from the diagnostic's measured diff --git a/apps/tv-native/src/lib/session-storage.ts b/apps/tv-native/src/lib/session-storage.ts new file mode 100644 index 00000000..d3e46433 --- /dev/null +++ b/apps/tv-native/src/lib/session-storage.ts @@ -0,0 +1,64 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { Platform, Settings } from "react-native"; + +export const isAppleTV = Platform.OS === "ios" && Platform.isTV; + +/** + * Durable key-value storage for the small session/config values, Apple-TV-aware. + * + * ## Why Apple TV is special + * On tvOS, `AsyncStorage` writes to `NSCachesDirectory`, which the OS PURGES when storage runs low after the + * app exits — so the server URL + session silently vanish and the TV drops back to onboarding after a reboot. + * The one tvOS store with a durability guarantee is **UserDefaults**, which React Native already exposes + * through its core **`Settings`** module (no native module, no extra dependency). So on Apple TV we back these + * values with `Settings`; every other platform keeps `AsyncStorage` unchanged. + * + * UserDefaults has no delete, so an **empty string is a tombstone**: `removeItem` writes `""`, and reads map + * `""` back to `null`. That also stops a stale (or not-yet-purged) legacy value from resurrecting a + * logged-out session or a previous server. + * + * ## Security note + * On Apple TV the bearer token already lived in (unencrypted) AsyncStorage, never the keychain — reading the + * keychain at startup crashed release builds (RN #54859) and tvOS keychain is unreliable across reboots + * anyway (see `cred-store.ts`). UserDefaults has the same confidentiality as that AsyncStorage fallback + * (plaintext inside the app sandbox) but is durable. iPad/iPhone/Android keep the real Keychain/Keystore via + * SecureStore for credentials. + */ + +/** Synchronous NSUserDefaults access via RN's core `Settings`. Empty string reads back as absent (tombstone). */ +export const TVSessionStorage = { + getItem(key: string): string | null { + const value = Settings.get(key); + return typeof value === "string" && value !== "" ? value : null; + }, + setItem(key: string, value: string): void { + Settings.set({ [key]: value }); + }, +}; + +/** + * Read a value, migrating a surviving pre-upgrade AsyncStorage value into UserDefaults once. A key that is + * already present in UserDefaults (including a `""` tombstone) is authoritative and never re-migrated. + */ +async function read(key: string): Promise { + const stored = Settings.get(key); + if (stored != null) return stored !== "" ? (stored as string) : null; + const legacy = await AsyncStorage.getItem(key); + TVSessionStorage.setItem(key, legacy ?? ""); // tombstone when the legacy value is also gone + return legacy; +} + +export const SessionStorage = isAppleTV + ? { + getItem: read, + async setItem(key: string, value: string): Promise { + TVSessionStorage.setItem(key, value); + }, + async removeItem(key: string): Promise { + // Clear any surviving legacy copy first, then write the durable tombstone. If cleanup throws the + // caller can retry without the durable store disagreeing with what's in memory. + await AsyncStorage.removeItem(key); + TVSessionStorage.setItem(key, ""); + }, + } + : AsyncStorage; diff --git a/apps/tv-roku/manifest b/apps/tv-roku/manifest index 99d3dfd9..10bbe4f2 100644 --- a/apps/tv-roku/manifest +++ b/apps/tv-roku/manifest @@ -3,7 +3,7 @@ title=Airwave # Version — kept in lockstep with the other apps/* by scripts/bump-version.ts (see /version-bump). major_version=0 minor_version=15 -build_version=65 +build_version=66 # Icons = branded logo mark on the radial gradient (home-screen tile). Splash = a FLAT #060a14 fill (Roku # requires a splash image, but ours hands off seamlessly to the in-app animated LogoLockup, which fades the diff --git a/apps/tv-roku/package.json b/apps/tv-roku/package.json index 8745eb7c..8180108a 100644 --- a/apps/tv-roku/package.json +++ b/apps/tv-roku/package.json @@ -1,6 +1,6 @@ { "name": "tv-roku", - "version": "0.15.65", + "version": "0.15.66", "private": true, "description": "Airwave Roku client (BrighterScript + SceneGraph). A direct port of tv-web/tv-native to Roku — strict visual + functional parity, its own separately-maintained codebase.", "scripts": { diff --git a/apps/tv-tauri/package.json b/apps/tv-tauri/package.json index e0ed8188..c54e09fa 100644 --- a/apps/tv-tauri/package.json +++ b/apps/tv-tauri/package.json @@ -1,6 +1,6 @@ { "name": "tv-tauri", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "scripts": { diff --git a/apps/tv-tauri/src-tauri/Cargo.lock b/apps/tv-tauri/src-tauri/Cargo.lock index 9384eed7..a4fce3d1 100644 --- a/apps/tv-tauri/src-tauri/Cargo.lock +++ b/apps/tv-tauri/src-tauri/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "airwave" -version = "0.15.65" +version = "0.15.66" dependencies = [ "futures", "gtk", diff --git a/apps/tv-tauri/src-tauri/Cargo.toml b/apps/tv-tauri/src-tauri/Cargo.toml index 99458bbe..94e4fa06 100644 --- a/apps/tv-tauri/src-tauri/Cargo.toml +++ b/apps/tv-tauri/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "airwave" -version = "0.15.65" +version = "0.15.66" description = "Airwave desktop client (Tauri + libmpv)" authors = ["Airwave"] license = "" diff --git a/apps/tv-tauri/src-tauri/tauri.conf.json b/apps/tv-tauri/src-tauri/tauri.conf.json index aa144a47..3bf95eb8 100644 --- a/apps/tv-tauri/src-tauri/tauri.conf.json +++ b/apps/tv-tauri/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Airwave Client", - "version": "0.15.65", + "version": "0.15.66", "identifier": "com.airwave.tvdesktop", "build": { "frontendDist": "../dist", diff --git a/apps/tv-web/package.json b/apps/tv-web/package.json index 2590e575..4a10c740 100644 --- a/apps/tv-web/package.json +++ b/apps/tv-web/package.json @@ -1,6 +1,6 @@ { "name": "tv-web", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "scripts": { diff --git a/apps/tv-web/public/appinfo.json b/apps/tv-web/public/appinfo.json index ab17fe6f..e1bf4777 100644 --- a/apps/tv-web/public/appinfo.json +++ b/apps/tv-web/public/appinfo.json @@ -1,6 +1,6 @@ { "id": "com.airwave.tv", - "version": "0.15.65", + "version": "0.15.66", "vendor": "Airwave", "type": "web", "main": "index.html", diff --git a/apps/tv-web/public/config.xml b/apps/tv-web/public/config.xml index cfa38b8b..ccd5d891 100644 --- a/apps/tv-web/public/config.xml +++ b/apps/tv-web/public/config.xml @@ -10,7 +10,7 @@ of Samsung TVs (raise it only to use newer TV web APIs). NB: XML comments must not contain a double hyphen, so avoid CLI flags here. --> - + Airwave diff --git a/apps/web/package.json b/apps/web/package.json index 3d7fbd3b..8dca6279 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.15.65", + "version": "0.15.66", "private": true, "type": "module", "scripts": { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 434a3b9c..965f28b4 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -27,7 +27,7 @@ param( ) $ErrorActionPreference = "Stop" -$InstallerVersion = "0.15.65" # kept in lockstep with the app version by scripts/bump-version.ts +$InstallerVersion = "0.15.66" # kept in lockstep with the app version by scripts/bump-version.ts $ImageRepo = "ghcr.io/quixomatic/airwave" $ComposeUrl = if ($env:AIRWAVE_COMPOSE_URL) { $env:AIRWAVE_COMPOSE_URL } else { "https://www.getairwave.tv/docker-compose.yml" } $Marker = ".airwave-install" diff --git a/scripts/install.sh b/scripts/install.sh index 00f84394..66210de3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -21,7 +21,7 @@ set -eu # ---- Constants ------------------------------------------------------------- -INSTALLER_VERSION="0.15.65" # kept in lockstep with the app version by scripts/bump-version.ts +INSTALLER_VERSION="0.15.66" # kept in lockstep with the app version by scripts/bump-version.ts IMAGE_REPO="ghcr.io/quixomatic/airwave" COMPOSE_URL="${AIRWAVE_COMPOSE_URL:-https://www.getairwave.tv/docker-compose.yml}" MARKER=".airwave-install"