From 8c10681d30310d65c05c520aedd6a626b021e50f Mon Sep 17 00:00:00 2001 From: venkateshsakamuri-lab Date: Thu, 27 Aug 2026 12:19:54 +0530 Subject: [PATCH] feat(web): add a public /download page for the desktop client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lists the desktop installers straight from the GitHub Releases API, grouped by platform with architecture detection and the visitor's own platform highlighted. Filters releases on the desktop-v tag prefix rather than using /releases/latest. The repo publishes two unrelated series from one tags list — v1.3.0 (the app) and desktop-v* (this client) — and /releases/latest returns the newest of either, so it hands back the app release and would point every download button at the wrong artifact. "No desktop build published yet" is kept distinct from "could not reach GitHub", so a failed fetch can never render as an empty catalogue. Adds /download to AUTH_PUBLIC_PATHS in both client.js and useAuth.jsx. Without it, useAuth's mount effect redirects any non-allowlisted path to /login when there is no session — bouncing exactly the logged-out visitors the page exists for. The page uses fetch rather than lib/api/client.js on purpose: that module is the DeepSQL backend's axios layer (auth headers, refresh, error envelope), none of which applies to a third-party public API, and the page must work with no session and a down backend. Co-Authored-By: Claude Opus 5 --- src/App.jsx | 4 + src/hooks/useAuth.jsx | 3 +- src/lib/api/client.js | 6 +- src/pages/Download.jsx | 286 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 src/pages/Download.jsx diff --git a/src/App.jsx b/src/App.jsx index bdd7a2a..eb9c7d7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import Login from './pages/Login' import Signup from './pages/Signup' import ActivateInvite from './pages/ActivateInvite' import CliAuthorize from './pages/CliAuthorize' +import Download from './pages/Download' import Onboarding from './pages/Onboarding' import PublicDashboardPage from './pages/PublicDashboardPage' import SharedDashboardPage from './pages/SharedDashboardPage' @@ -146,6 +147,9 @@ function App() { /> {/* Legacy /setup route — now the real onboarding wizard, not a dead end. */} } /> + {/* Public desktop-client download page — no login: it is reached from + the marketing site by people who do not have an account yet. */} + } /> } /> } /> diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx index fca85c5..799a875 100644 --- a/src/hooks/useAuth.jsx +++ b/src/hooks/useAuth.jsx @@ -14,7 +14,8 @@ import { PERMISSIONS, ROLES, ROLE_BASELINE_PERMISSIONS, normalizeRole, isAdminRo const AuthContext = createContext(null) -const AUTH_PUBLIC_PATHS = ['/login', '/signup', '/activate'] +// See the matching list in lib/api/client.js — '/download' is public. +const AUTH_PUBLIC_PATHS = ['/login', '/signup', '/activate', '/download'] const isPublicAuthPath = (pathname) => AUTH_PUBLIC_PATHS.some((prefix) => pathname.startsWith(prefix)) diff --git a/src/lib/api/client.js b/src/lib/api/client.js index 558d6bc..d98c513 100644 --- a/src/lib/api/client.js +++ b/src/lib/api/client.js @@ -13,7 +13,11 @@ const getApiBaseUrl = () => { export const API_BASE_URL = getApiBaseUrl(); export const AUTH_CHANGE_EVENT = "deepsql-auth-change"; -const AUTH_PUBLIC_PATHS = ["/login", "/signup", "/activate"]; +// "/download" is reachable with no session on purpose: it is the public +// desktop-client download page, linked from the marketing site by people who +// do not have an account yet. Without it here, a logged-out visitor is bounced +// to /login and never sees the installers. +const AUTH_PUBLIC_PATHS = ["/login", "/signup", "/activate", "/download"]; const isPublicAuthPath = (pathname = "") => AUTH_PUBLIC_PATHS.some((prefix) => pathname.startsWith(prefix)); diff --git a/src/pages/Download.jsx b/src/pages/Download.jsx new file mode 100644 index 0000000..ca8469b --- /dev/null +++ b/src/pages/Download.jsx @@ -0,0 +1,286 @@ +import { useEffect, useMemo, useState } from 'react' +import { + AlertTriangle, + Apple, + Download as DownloadIcon, + Loader2, + Monitor, + Package, + Terminal, +} from 'lucide-react' + +/** + * Public download page for the DeepSQL desktop client. + * + * Asset list comes straight from the GitHub Releases API. This is the one place + * that deliberately does NOT go through lib/api/client.js: that module is the + * DeepSQL backend's axios layer (auth headers, refresh, error envelope), and + * none of it applies to a third-party public API. A plain fetch keeps the page + * working before a user has logged in — or on a box whose backend is down. + * + * The repo publishes two unrelated release series from the same tags list: + * `v1.3.0` (the DeepSQL app) and `desktop-v*` (this client). /releases/latest + * returns the newest of *either*, so it hands back the app release and would + * point every download button at the wrong artifact. Filter by tag prefix. + */ + +const REPO = 'DeepSQLAI/deepsql' +const TAG_PREFIX = 'desktop-v' + +const PLATFORMS = { + mac: { label: 'macOS', icon: Apple }, + windows: { label: 'Windows', icon: Monitor }, + linux: { label: 'Linux', icon: Terminal }, +} + +/** Classify a release asset by filename, not by position in the list. */ +function classify(asset) { + const name = asset.name.toLowerCase() + const arch = name.includes('arm64') + ? 'Apple Silicon' + : name.includes('x64') || name.includes('amd64') || name.includes('x86_64') + ? 'Intel / AMD64' + : null + + if (name.endsWith('.dmg')) return { platform: 'mac', kind: 'Disk image', arch } + if (name.endsWith('.zip')) return { platform: 'mac', kind: 'Zip archive', arch } + if (name.endsWith('.exe')) + return { + platform: 'windows', + kind: name.includes('setup') ? 'Installer' : 'Portable', + arch, + } + if (name.endsWith('.appimage')) return { platform: 'linux', kind: 'AppImage', arch } + if (name.endsWith('.deb')) return { platform: 'linux', kind: 'Debian package', arch } + if (name.endsWith('.rpm')) return { platform: 'linux', kind: 'RPM package', arch } + return null +} + +/** Best-effort guess so the primary button matches the visitor's machine. */ +function detectPlatform() { + const ua = navigator.userAgent || '' + if (/Mac|iPhone|iPad/i.test(ua)) return 'mac' + if (/Win/i.test(ua)) return 'windows' + if (/Linux|X11/i.test(ua)) return 'linux' + return null +} + +function formatSize(bytes) { + if (!bytes) return '' + const mb = bytes / (1024 * 1024) + return `${mb.toFixed(1)} MB` +} + +export default function Download() { + const [state, setState] = useState({ status: 'loading' }) + const detected = useMemo(() => detectPlatform(), []) + + useEffect(() => { + let cancelled = false + + fetch(`https://api.github.com/repos/${REPO}/releases?per_page=30`, { + headers: { Accept: 'application/vnd.github+json' }, + }) + .then((res) => { + if (!res.ok) throw new Error(`GitHub returned ${res.status}`) + return res.json() + }) + .then((releases) => { + if (cancelled) return + const release = releases.find( + (r) => r.tag_name?.startsWith(TAG_PREFIX) && !r.draft, + ) + // No desktop release yet is a *different* answer from "we could not + // check", and the page must not blur the two into one empty state. + if (!release) return setState({ status: 'none' }) + setState({ status: 'ready', release }) + }) + .catch((err) => { + if (cancelled) return + setState({ status: 'error', message: err.message }) + }) + + return () => { + cancelled = true + } + }, []) + + const grouped = useMemo(() => { + if (state.status !== 'ready') return {} + const out = { mac: [], windows: [], linux: [] } + for (const asset of state.release.assets || []) { + const meta = classify(asset) + if (meta) out[meta.platform].push({ ...asset, ...meta }) + } + return out + }, [state]) + + return ( +
+
+
+
+
+ +
+

DeepSQL Desktop

+
+

+ A native client for your self-hosted DeepSQL server. Connects directly over + TLS or through an SSH tunnel, with connection health and transport status + built into the window chrome. +

+
+ + {state.status === 'loading' && ( +
+ + Looking up the latest release… +
+ )} + + {state.status === 'error' && ( + + )} + + {state.status === 'none' && ( + + )} + + {state.status === 'ready' && ( + <> +
+ + {state.release.tag_name.replace(TAG_PREFIX, 'Version ')} + + + released {new Date(state.release.published_at).toLocaleDateString()} + +
+ + {Object.entries(PLATFORMS).map(([key, meta]) => { + const assets = grouped[key] || [] + if (!assets.length) return null + return ( + + ) + })} + +

+ Source and build instructions live in{' '} + + desktop/README.md + + . +

+ + )} +
+
+ ) +} + +function PlatformSection({ platform, assets, highlight, showMacNote }) { + const Icon = platform.icon + return ( +
+
+ +

+ {platform.label} +

+ {highlight && ( + + Detected + + )} +
+ + + + {showMacNote && ( +

+ Builds are unsigned unless signing credentials are configured, so the first + launch needs right-click → Open (or{' '} + xattr -dr com.apple.quarantine /Applications/DeepSQL.app + ). +

+ )} +
+ ) +} + +function Notice({ tone, title, body, action }) { + return ( +
+
+ +
+

{title}

+

{body}

+ {action && ( + + {action.label} + + )} +
+
+
+ ) +}