Skip to content

Commit ce9a7cd

Browse files
feat: enhance desktop download page and add login discoverability
- Improve Download page with platform detection, primary CTA, and asset ranking - Link to /download from Login sidebar and footer - Document downloadable installers in README Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent 3136574 commit ce9a7cd

3 files changed

Lines changed: 124 additions & 22 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,13 @@ your schema — all from one shared brain.
248248
### DeepSQL Desktop (optional)
249249

250250
A thin Electron client for a self-hosted VM — direct TLS or an in-process SSH tunnel —
251-
without bundling a second copy of the web UI. Separate npm project:
251+
without bundling a second copy of the web UI.
252+
253+
**Download installers** (macOS, Windows, Linux) from the public page at `/download` on
254+
your DeepSQL instance, or browse
255+
[GitHub Releases](https://github.com/DeepSQLAI/deepsql/releases) for `desktop-v*` tags.
256+
257+
To build from source (separate npm project):
252258

253259
```bash
254260
cd desktop

src/pages/Download.jsx

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useEffect, useMemo, useState } from 'react'
2+
import { Link } from 'react-router-dom'
23
import {
34
AlertTriangle,
45
Apple,
6+
ArrowLeft,
57
Download as DownloadIcon,
68
Loader2,
79
Monitor,
@@ -42,17 +44,18 @@ function classify(asset) {
4244
? 'Intel / AMD64'
4345
: null
4446

45-
if (name.endsWith('.dmg')) return { platform: 'mac', kind: 'Disk image', arch }
46-
if (name.endsWith('.zip')) return { platform: 'mac', kind: 'Zip archive', arch }
47+
if (name.endsWith('.dmg')) return { platform: 'mac', kind: 'Disk image', arch, rank: 0 }
48+
if (name.endsWith('.zip')) return { platform: 'mac', kind: 'Zip archive', arch, rank: 1 }
4749
if (name.endsWith('.exe'))
4850
return {
4951
platform: 'windows',
5052
kind: name.includes('setup') ? 'Installer' : 'Portable',
5153
arch,
54+
rank: name.includes('setup') ? 0 : 1,
5255
}
53-
if (name.endsWith('.appimage')) return { platform: 'linux', kind: 'AppImage', arch }
54-
if (name.endsWith('.deb')) return { platform: 'linux', kind: 'Debian package', arch }
55-
if (name.endsWith('.rpm')) return { platform: 'linux', kind: 'RPM package', arch }
56+
if (name.endsWith('.appimage')) return { platform: 'linux', kind: 'AppImage', arch, rank: 1 }
57+
if (name.endsWith('.deb')) return { platform: 'linux', kind: 'Debian package', arch, rank: 0 }
58+
if (name.endsWith('.rpm')) return { platform: 'linux', kind: 'RPM package', arch, rank: 2 }
5659
return null
5760
}
5861

@@ -65,12 +68,44 @@ function detectPlatform() {
6568
return null
6669
}
6770

71+
/** Prefer Apple Silicon builds on arm64 Macs when both exist. */
72+
function detectMacArchPreference() {
73+
const ua = navigator.userAgent || ''
74+
if (/arm64|aarch64/i.test(ua)) return 'Apple Silicon'
75+
return 'Intel / AMD64'
76+
}
77+
6878
function formatSize(bytes) {
6979
if (!bytes) return ''
7080
const mb = bytes / (1024 * 1024)
7181
return `${mb.toFixed(1)} MB`
7282
}
7383

84+
function pickPrimaryAsset(grouped, platform) {
85+
const assets = grouped[platform] || []
86+
if (!assets.length) return null
87+
88+
const archPref = platform === 'mac' ? detectMacArchPreference() : null
89+
const sorted = [...assets].sort((a, b) => {
90+
if (archPref) {
91+
const aMatch = a.arch === archPref ? 0 : 1
92+
const bMatch = b.arch === archPref ? 0 : 1
93+
if (aMatch !== bMatch) return aMatch - bMatch
94+
}
95+
return (a.rank ?? 9) - (b.rank ?? 9)
96+
})
97+
return sorted[0]
98+
}
99+
100+
function latestDesktopRelease(releases) {
101+
return releases
102+
.filter((r) => r.tag_name?.startsWith(TAG_PREFIX) && !r.draft)
103+
.sort(
104+
(a, b) =>
105+
new Date(b.published_at).getTime() - new Date(a.published_at).getTime(),
106+
)[0]
107+
}
108+
74109
export default function Download() {
75110
const [state, setState] = useState({ status: 'loading' })
76111
const detected = useMemo(() => detectPlatform(), [])
@@ -87,11 +122,7 @@ export default function Download() {
87122
})
88123
.then((releases) => {
89124
if (cancelled) return
90-
const release = releases.find(
91-
(r) => r.tag_name?.startsWith(TAG_PREFIX) && !r.draft,
92-
)
93-
// No desktop release yet is a *different* answer from "we could not
94-
// check", and the page must not blur the two into one empty state.
125+
const release = latestDesktopRelease(releases)
95126
if (!release) return setState({ status: 'none' })
96127
setState({ status: 'ready', release })
97128
})
@@ -112,12 +143,33 @@ export default function Download() {
112143
const meta = classify(asset)
113144
if (meta) out[meta.platform].push({ ...asset, ...meta })
114145
}
146+
for (const key of Object.keys(out)) {
147+
out[key].sort((a, b) => (a.rank ?? 9) - (b.rank ?? 9))
148+
}
115149
return out
116150
}, [state])
117151

152+
const primary = useMemo(
153+
() => (detected ? pickPrimaryAsset(grouped, detected) : null),
154+
[grouped, detected],
155+
)
156+
157+
const versionLabel =
158+
state.status === 'ready'
159+
? state.release.tag_name.replace(TAG_PREFIX, 'Version ')
160+
: null
161+
118162
return (
119163
<div className="min-h-screen bg-white text-gray-900">
120164
<div className="max-w-3xl mx-auto px-6 py-16">
165+
<Link
166+
to="/login"
167+
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-800 transition-colors mb-8"
168+
>
169+
<ArrowLeft className="h-4 w-4" />
170+
Back to sign in
171+
</Link>
172+
121173
<header className="mb-12">
122174
<div className="flex items-center gap-3 mb-4">
123175
<div className="h-10 w-10 rounded-xl bg-gray-900 flex items-center justify-center">
@@ -144,7 +196,10 @@ export default function Download() {
144196
tone="error"
145197
title="Could not reach GitHub"
146198
body={`The download list could not be loaded (${state.message}). This is a problem fetching the release list, not a sign that no build exists — you can browse releases directly on GitHub.`}
147-
action={{ href: `https://github.com/${REPO}/releases`, label: 'Open releases on GitHub' }}
199+
action={{
200+
href: `https://github.com/${REPO}/releases`,
201+
label: 'Open releases on GitHub',
202+
}}
148203
/>
149204
)}
150205

@@ -153,21 +208,38 @@ export default function Download() {
153208
tone="info"
154209
title="No desktop build published yet"
155210
body="The release list loaded fine — there is simply no desktop-v* release with attached installers. Builds are produced by the desktop-release workflow when a desktop-v* tag is pushed."
156-
action={{ href: `https://github.com/${REPO}/releases`, label: 'Open releases on GitHub' }}
211+
action={{
212+
href: `https://github.com/${REPO}/releases`,
213+
label: 'Open releases on GitHub',
214+
}}
157215
/>
158216
)}
159217

160218
{state.status === 'ready' && (
161219
<>
162-
<div className="flex items-baseline gap-3 mb-8 pb-4 border-b border-gray-200">
163-
<span className="text-sm font-semibold text-gray-900">
164-
{state.release.tag_name.replace(TAG_PREFIX, 'Version ')}
165-
</span>
220+
<div className="flex flex-wrap items-baseline gap-3 mb-6 pb-4 border-b border-gray-200">
221+
<span className="text-sm font-semibold text-gray-900">{versionLabel}</span>
166222
<span className="text-sm text-gray-400">
167223
released {new Date(state.release.published_at).toLocaleDateString()}
168224
</span>
169225
</div>
170226

227+
{primary && (
228+
<div className="mb-10">
229+
<a
230+
href={primary.browser_download_url}
231+
className="inline-flex w-full sm:w-auto items-center justify-center gap-2 rounded-full bg-gray-900 px-6 py-3.5 text-sm font-semibold text-white shadow-lg transition-all hover:bg-gray-800 active:scale-[0.98]"
232+
>
233+
<DownloadIcon className="h-4 w-4" />
234+
Download for {PLATFORMS[detected].label}
235+
{primary.arch ? ` (${primary.arch})` : ''}
236+
</a>
237+
<p className="mt-2 text-xs text-gray-400">
238+
{primary.kind} · {primary.name} · {formatSize(primary.size)}
239+
</p>
240+
</div>
241+
)}
242+
171243
{Object.entries(PLATFORMS).map(([key, meta]) => {
172244
const assets = grouped[key] || []
173245
if (!assets.length) return null
@@ -190,6 +262,13 @@ export default function Download() {
190262
>
191263
desktop/README.md
192264
</a>
265+
. All installers for this release are also on{' '}
266+
<a
267+
className="underline hover:text-gray-600 transition-colors"
268+
href={state.release.html_url}
269+
>
270+
GitHub
271+
</a>
193272
.
194273
</p>
195274
</>
@@ -227,7 +306,9 @@ function PlatformSection({ platform, assets, highlight, showMacNote }) {
227306
}`}
228307
>
229308
<span className="flex items-center gap-3 min-w-0">
230-
<Package className={`h-4 w-4 shrink-0 ${highlight ? 'text-gray-300' : 'text-gray-400'}`} />
309+
<Package
310+
className={`h-4 w-4 shrink-0 ${highlight ? 'text-gray-300' : 'text-gray-400'}`}
311+
/>
231312
<span className="min-w-0">
232313
<span className="block text-sm font-medium truncate">
233314
{asset.kind}
@@ -251,7 +332,9 @@ function PlatformSection({ platform, assets, highlight, showMacNote }) {
251332
<p className="mt-3 text-xs text-gray-400 leading-relaxed">
252333
Builds are unsigned unless signing credentials are configured, so the first
253334
launch needs <span className="text-gray-600">right-click → Open</span> (or{' '}
254-
<code className="text-gray-600">xattr -dr com.apple.quarantine /Applications/DeepSQL.app</code>
335+
<code className="text-gray-600">
336+
xattr -dr com.apple.quarantine /Applications/DeepSQL.app
337+
</code>
255338
).
256339
</p>
257340
)}

src/pages/Login.jsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
22
import { Link, useSearchParams } from 'react-router-dom'
33
import { useAuth } from '@/hooks/useAuth'
44
import { authAPI, setupAPI } from '@/lib/api/client'
5-
import { ArrowLeft, Database, KeyRound, Mail, ShieldCheck, Sparkles, Zap, Activity, LineChart } from 'lucide-react'
5+
import { ArrowLeft, Database, Download, KeyRound, Mail, ShieldCheck, Sparkles, Zap, Activity, LineChart } from 'lucide-react'
66

77
const STEP_LOGIN = 'login'
88
const STEP_OTP = 'otp'
@@ -303,6 +303,14 @@ export default function Login() {
303303
</div>
304304
))}
305305
</div>
306+
307+
<Link
308+
to="/download"
309+
className="mt-10 inline-flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
310+
>
311+
<Download className="w-4 h-4" />
312+
Download DeepSQL Desktop
313+
</Link>
306314
</div>
307315
</div>
308316

@@ -352,8 +360,13 @@ export default function Login() {
352360
)}
353361
</div>
354362

355-
<div className="absolute bottom-6 left-0 right-0 text-center text-gray-500 text-sm px-6">
356-
&copy; 2026 DeepSQL. Built for developers who love databases.
363+
<div className="absolute bottom-6 left-0 right-0 text-center text-gray-500 text-sm px-6 space-y-1">
364+
<p>
365+
<Link to="/download" className="text-gray-600 hover:text-gray-900 underline underline-offset-2">
366+
Download DeepSQL Desktop
367+
</Link>
368+
</p>
369+
<p>&copy; 2026 DeepSQL. Built for developers who love databases.</p>
357370
</div>
358371
</div>
359372
</div>

0 commit comments

Comments
 (0)