Skip to content

Commit b8e5623

Browse files
matej21claude
andcommitted
feat: add "Load Demo Database" button for first-time desktop users
When no connections exist, the sidebar now shows a "Load Demo Database" button that copies the pre-built bookstore SQLite database to the user data directory, creates a connection, and auto-connects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d47bf52 commit b8e5623

File tree

10 files changed

+96
-2
lines changed

10 files changed

+96
-2
lines changed

electrobun.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default {
1313
copy: {
1414
'dist/index.html': 'views/mainview/index.html',
1515
'dist/assets': 'views/mainview/assets',
16+
'scripts/seed/bookstore.db': 'resources/bookstore.db',
1617
},
1718
watchIgnore: ['dist/**'],
1819
mac: {

src/backend-desktop/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AppDatabase, setDefaultDbPath } from '@dotaz/backend-shared/storage/app
55
import type { DotazRPC } from '@dotaz/backend-types'
66
import { BrowserView, BrowserWindow, Updater, Utils } from 'electrobun/bun'
77
import { existsSync, mkdirSync } from 'node:fs'
8-
import { join } from 'node:path'
8+
import { join, resolve } from 'node:path'
99

1010
const DEV_SERVER_PORT = 6400
1111
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
@@ -46,8 +46,15 @@ const connectionManager = new ConnectionManager(appDb)
4646

4747
// Create RPC handlers with deferred message emitter (set after window creation)
4848
let emitToFrontend: ((channel: string, payload: unknown) => void) | undefined
49+
const userDataDir = Utils.paths.userData
50+
// Demo DB: try dev-time path first, then bundled resource next to the executable
51+
const devDemoPath = resolve(import.meta.dir, '../../scripts/seed/bookstore.db')
52+
const bundledDemoPath = resolve(import.meta.dir, '../resources/bookstore.db')
53+
const demoDbSourcePath = existsSync(devDemoPath) ? devDemoPath : bundledDemoPath
4954
const { handlers, sessionManager } = createHandlers(connectionManager, undefined, appDb, Utils, {
5055
emitMessage: (channel, payload) => emitToFrontend?.(channel, payload),
56+
demoDbSourcePath,
57+
demoDbTargetPath: join(userDataDir, 'bookstore-demo.db'),
5158
})
5259
const rpc = BrowserView.defineRPC<DotazRPC>({
5360
maxRequestTime: 30000,
@@ -112,7 +119,6 @@ connectionManager.onStatusChanged((event) => {
112119
}
113120
})
114121

115-
116122
// ── Auto-update ──────────────────────────────────────────
117123
const currentChannel = await Updater.localInfo.channel()
118124
if (currentChannel !== 'dev') {

src/backend-shared/rpc/adapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,7 @@ export interface RpcAdapter {
129129
// ── Workspace persistence ─────────────────────────────
130130
saveWorkspace(data: string): void
131131
loadWorkspace(): string | null
132+
133+
// ── Demo ──────────────────────────────────────────────
134+
initializeDemo?(): Promise<ConnectionInfo>
132135
}

src/backend-shared/rpc/backend-adapter.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface BackendAdapterOptions {
3939
Utils?: typeof import('electrobun/bun').Utils
4040
emitMessage?: EmitMessage
4141
sessionManager?: SessionManager
42+
demoDbSourcePath?: string
43+
demoDbTargetPath?: string
4244
}
4345

4446
export class BackendAdapter implements RpcAdapter {
@@ -47,6 +49,8 @@ export class BackendAdapter implements RpcAdapter {
4749
private Utils?: typeof import('electrobun/bun').Utils
4850
private emitMessage?: EmitMessage
4951
private sessionManager?: SessionManager
52+
private demoDbSourcePath?: string
53+
private demoDbTargetPath?: string
5054

5155
constructor(
5256
private cm: ConnectionManager,
@@ -59,6 +63,8 @@ export class BackendAdapter implements RpcAdapter {
5963
this.Utils = opts?.Utils
6064
this.emitMessage = opts?.emitMessage
6165
this.sessionManager = opts?.sessionManager
66+
this.demoDbSourcePath = opts?.demoDbSourcePath
67+
this.demoDbTargetPath = opts?.demoDbTargetPath
6268
}
6369

6470
// ── Connections ────────────────────────────────────────
@@ -591,6 +597,27 @@ export class BackendAdapter implements RpcAdapter {
591597
return this.appDb.loadWorkspace()
592598
}
593599

600+
// ── Demo ──────────────────────────────────────────────
601+
602+
async initializeDemo(): Promise<ConnectionInfo> {
603+
if (!this.demoDbSourcePath || !this.demoDbTargetPath) {
604+
throw new Error('Demo database paths not configured')
605+
}
606+
607+
const srcFile = Bun.file(this.demoDbSourcePath)
608+
if (!await srcFile.exists()) {
609+
throw new Error('Demo database source not found. Run "bun run seed:sqlite" first.')
610+
}
611+
612+
await Bun.write(this.demoDbTargetPath, srcFile)
613+
614+
const config = { type: 'sqlite' as const, path: this.demoDbTargetPath }
615+
const conn = this.appDb.createConnection({ name: 'Bookstore (Demo)', config })
616+
617+
await this.cm.connect(conn.id)
618+
return conn
619+
}
620+
594621
// ── Session Manager access ────────────────────────────
595622

596623
getSessionManager(): SessionManager | undefined {

src/backend-shared/rpc/handlers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,5 +313,13 @@ export function createHandlers(adapter: RpcAdapter) {
313313
}
314314
return adapter.showSaveDialog(params)
315315
},
316+
317+
// ── Demo ──────────────────────────────────────────────
318+
'demo.initialize': async () => {
319+
if (!adapter.initializeDemo) {
320+
throw new Error('Demo initialization is not available')
321+
}
322+
return adapter.initializeDemo()
323+
},
316324
} as const
317325
}

src/backend-shared/rpc/rpc-handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { createHandlers as createSharedHandlers } from './handlers'
99
export interface HandlerOptions {
1010
encryption?: EncryptionService
1111
emitMessage?: (channel: string, payload: unknown) => void
12+
demoDbSourcePath?: string
13+
demoDbTargetPath?: string
1214
}
1315

1416
function requireAppDb(appDb: AppDatabase | undefined): AppDatabase {
@@ -31,6 +33,8 @@ export function createHandlers(
3133
Utils,
3234
emitMessage: opts?.emitMessage,
3335
sessionManager,
36+
demoDbSourcePath: opts?.demoDbSourcePath,
37+
demoDbTargetPath: opts?.demoDbTargetPath,
3438
})
3539
return { handlers: createSharedHandlers(adapter), sessionManager, adapter }
3640
}

src/frontend-demo/demo-adapter.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,15 @@ export class DemoAdapter implements RpcAdapter {
551551
return null
552552
}
553553

554+
// ── Demo ──────────────────────────────────────────────
555+
556+
async initializeDemo(): Promise<ConnectionInfo> {
557+
// Demo mode already has the bookstore connection
558+
const connections = this.state.listConnections()
559+
if (connections.length > 0) return connections[0]
560+
throw new Error('Demo connection not found')
561+
}
562+
554563
// ── Private ──────────────────────────────────────────
555564

556565
private logHistory(connectionId: string, sql: string, results: QueryResult[]): void {

src/frontend-shared/components/connection/ConnectionTree.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@
102102
background: var(--accent-hover);
103103
}
104104

105+
.connection-tree__empty-demo {
106+
background: transparent;
107+
color: var(--ink-muted);
108+
border: 1px solid var(--edge);
109+
padding: var(--spacing-xs) var(--spacing-md);
110+
border-radius: 3px;
111+
cursor: pointer;
112+
font-size: var(--font-size-xs);
113+
font-family: var(--font-ui);
114+
}
115+
116+
.connection-tree__empty-demo:hover {
117+
color: var(--ink);
118+
border-color: var(--ink-muted);
119+
}
120+
105121
/* ── TreeItem ────────────────────────────────────────── */
106122

107123
.tree-item {

src/frontend-shared/components/connection/ConnectionTree.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,18 @@ export default function ConnectionTree(props: ConnectionTreeProps) {
735735
<button class="connection-tree__empty-cta" onClick={props.onAddConnection}>
736736
<Plus size={14} /> Add Connection
737737
</button>
738+
<button
739+
class="connection-tree__empty-demo"
740+
onClick={async () => {
741+
try {
742+
await connectionsStore.initializeDemo()
743+
} catch (err) {
744+
uiStore.addToast('error', err instanceof Error ? err.message : 'Failed to load demo')
745+
}
746+
}}
747+
>
748+
Load Demo Database
749+
</button>
738750
</div>
739751
}
740752
>

src/frontend-shared/stores/connections.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,13 @@ async function deleteConnection(id: string) {
245245
}
246246
}
247247

248+
async function initializeDemo(): Promise<ConnectionInfo> {
249+
const conn = await rpc.demo.initialize()
250+
setState('connections', (prev) => [...prev, conn])
251+
// Connection status will be updated via the statusChanged event
252+
return conn
253+
}
254+
248255
async function connectTo(id: string, password?: string) {
249256
// If adapter needs config on connect and password not remembered, prompt for it
250257
if (storage.passConfigOnConnect && !password) {
@@ -444,6 +451,7 @@ export const connectionsStore = {
444451
},
445452
getRememberPassword,
446453
loadConnections,
454+
initializeDemo,
447455
createConnection,
448456
updateConnection,
449457
setReadOnly,

0 commit comments

Comments
 (0)