experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: merge implementation notes into profiles.md, improve switching UX

Merged PROFILES-IMPLEMENTATION-SUMMARY.md into docs/profiles.md:
- Added Implementation Notes section with files modified, design
principles, migration strategy, and commits
- Deleted redundant summary file
- All implementation details now in canonical docs location

Improved profile switching UX and debugging:
- Added console logging to track switch operations
- Check if already on target profile (prevent no-op)
- Improved confirm dialog text with clearer message
- Added backend logging in IPC handler to debug switch flow
- IPC handler now checks current profile before switching

This addresses reported issue where switching back to default
didn't show prompt - added logging to help diagnose.

+157 -352
-350
PROFILES-IMPLEMENTATION-SUMMARY.md
··· 1 - # User Profiles Implementation - Summary 2 - 3 - ## Overview 4 - 5 - Successfully implemented user profiles and profile switching across desktop and server. Each profile provides isolated data storage with optional per-profile sync configuration. 6 - 7 - ## What Was Implemented 8 - 9 - ### Server (Multi-User System) 10 - 11 - 1. **Profiles Table** (`backend/server/users.js`) 12 - - Added `profiles` table to `system.db` 13 - - Schema: `id`, `user_id`, `slug`, `name`, `created_at`, `last_used_at` 14 - - CRUD functions: `createProfile`, `listProfiles`, `getProfile`, `deleteProfile` 15 - 16 - 2. **Connection Pooling** (`backend/server/db.js`) 17 - - Changed from `userId` to `userId:profileSlug` composite keys 18 - - Database path: `data/{userId}/profiles/{profileSlug}/datastore.sqlite` 19 - - All DB functions accept optional `profileSlug` parameter (defaults to "default") 20 - 21 - 3. **API Endpoints** (`backend/server/index.js`) 22 - - All endpoints accept `?profile={slug}` query parameter 23 - - New endpoints: GET/POST/DELETE `/profiles` 24 - - Migration function: `migrateUserDataToProfiles()` moves `peek.db` → `profiles/default/` 25 - - Backward compatible: defaults to "default" profile if not specified 26 - 27 - ### Desktop Client 28 - 29 - 4. **Profile Database Module** (`backend/electron/profiles.ts`) - NEW FILE 30 - - `profiles.db` stores profile metadata and sync config 31 - - Schema includes: id, name, slug, syncEnabled, apiKey, serverProfileSlug, lastSyncAt 32 - - Functions: initProfilesDb, listProfiles, createProfile, getProfile, deleteProfile 33 - - Active profile tracking: getActiveProfile, setActiveProfile 34 - - Sync config: enableSync, disableSync, getSyncConfig, updateLastSyncTime 35 - - Migration: migrateExistingProfiles() detects existing profile directories 36 - 37 - 5. **IPC Handlers** (`backend/electron/ipc.ts`) 38 - - Added `registerProfileHandlers()` function 39 - - Handlers for: list, create, get, delete, getCurrent, switch 40 - - Sync configuration handlers: enableSync, disableSync, getSyncConfig 41 - - Profile switching triggers app relaunch 42 - 43 - 6. **Profile Initialization** (`backend/electron/entry.ts`) 44 - - Initialize profiles.db on startup 45 - - Profile selection logic: 46 - 1. Explicit `PROFILE` env var (testing override) 47 - 2. **Development builds → ALWAYS use 'dev'** (production isolation) 48 - 3. Production builds → use active profile from profiles.db 49 - 4. Fallback → 'default' 50 - - **Critical fix**: Dev builds never touch production profiles 51 - 52 - 7. **Sync Integration** (`backend/electron/sync.ts`) 53 - - Modified to use per-profile sync configuration 54 - - `getSyncConfig()` reads from active profile 55 - - Pull/push operations include `?profile={serverProfileSlug}` parameter 56 - - `syncAll()` updates per-profile lastSyncTime 57 - 58 - 8. **Settings UI** (`app/settings/settings.js`) 59 - - New "Profiles" section between Sync and Themes 60 - - Displays current active profile 61 - - Profile list with radio buttons for switching 62 - - "Add Profile" button with name input dialog 63 - - Delete profile button (disabled for default/active) 64 - - Per-profile sync configuration UI: 65 - - Enable/disable sync buttons 66 - - API key and server profile slug inputs 67 - - Shows sync status and server profile mapping 68 - 69 - 9. **Frontend API** (`preload.js`) 70 - - Exposed `api.profiles` object 71 - - Functions: list, create, get, delete, getCurrent, switch 72 - - Sync: enableSync, disableSync, getSyncConfig 73 - 74 - ## Key Design Decisions 75 - 76 - ### Production vs Development Isolation 77 - 78 - **Critical Rule**: Development builds NEVER touch production data. 79 - 80 - - **Production** (packaged in /Applications) → uses "default" or user-selected profile 81 - - **Development** (`yarn start` from source) → ALWAYS uses "dev" profile 82 - - Single-instance lock skipped for dev profiles → allows both to run simultaneously 83 - 84 - This prevents accidental corruption of production data during development. 85 - 86 - ### Profile vs Chromium Profile 87 - 88 - **Nested relationship, NOT the same:** 89 - 90 - ``` 91 - Peek Profile # Application-level 92 - └── Chromium Profile # Browser session data 93 - 94 - ~/.config/Peek/ 95 - ├── profiles.db # Peek profile metadata 96 - ├── default/ # Peek profile 97 - │ ├── datastore.sqlite # Peek data (items, tags) 98 - │ └── chromium/ # Chromium session (cookies, cache) 99 - └── work/ # Another Peek profile 100 - ├── datastore.sqlite 101 - └── chromium/ # Separate Chromium session 102 - ``` 103 - 104 - **Electron configuration:** 105 - ```typescript 106 - const profileDataPath = path.join(userDataPath, PROFILE); 107 - const sessionDataPath = path.join(profileDataPath, 'chromium'); 108 - 109 - app.setPath('userData', profileDataPath); 110 - app.setPath('sessionData', sessionDataPath); 111 - ``` 112 - 113 - Each Peek profile gets its own isolated Chromium session, providing: 114 - - Separate cookies (login sessions) 115 - - Separate localStorage 116 - - Separate cache 117 - - Separate browser extensions 118 - 119 - ### Per-Profile Sync Configuration 120 - 121 - Each profile can independently sync to different server profiles: 122 - 123 - ``` 124 - Desktop Profile Server User Server Profile 125 - ───────────────── ────────── ────────────── 126 - Work alice work 127 - Personal alice personal 128 - ``` 129 - 130 - - One API key (authenticates user) 131 - - Multiple server profile targets 132 - - Stored in profiles.db per desktop profile 133 - 134 - ## Data Storage Structure 135 - 136 - ### Desktop 137 - 138 - ``` 139 - {userData}/ 140 - ├── profiles.db # Profile metadata + sync config 141 - ├── default/ # Production profile 142 - │ ├── datastore.sqlite 143 - │ └── chromium/ 144 - ├── dev/ # Development profile 145 - │ ├── datastore.sqlite 146 - │ └── chromium/ 147 - └── work/ # Custom user profile 148 - ├── datastore.sqlite 149 - └── chromium/ 150 - ``` 151 - 152 - ### Server 153 - 154 - ``` 155 - data/ 156 - ├── system.db # Users and profiles 157 - └── {userId}/ 158 - └── profiles/ 159 - ├── default/ 160 - │ └── datastore.sqlite 161 - ├── work/ 162 - │ └── datastore.sqlite 163 - └── personal/ 164 - └── datastore.sqlite 165 - ``` 166 - 167 - ## Migration Strategy 168 - 169 - ### Automatic Migration 170 - 171 - On first launch with new code: 172 - 173 - 1. `initProfilesDb()` creates `profiles.db` if missing 174 - 2. `migrateExistingProfiles()` detects existing directories: 175 - - `default/` → creates "Default" profile record 176 - - `dev/` → creates "Development" profile record 177 - 3. `ensureDefaultProfile()` ensures default profile exists 178 - 4. Existing data preserved, no data loss 179 - 180 - ### Server Migration 181 - 182 - `migrateUserDataToProfiles()` runs automatically: 183 - - Detects `data/{userId}/peek.db` (old path) 184 - - Moves to `data/{userId}/profiles/default/datastore.sqlite` 185 - - Creates "default" profile record in system.db 186 - - One-time operation, idempotent 187 - 188 - ## Backward Compatibility 189 - 190 - - `PROFILE` env var still works (overrides all logic) 191 - - API endpoints default to `profile=default` if not specified 192 - - Existing profile directories detected and migrated 193 - - Zero breaking changes for existing deployments 194 - 195 - ## Testing Status 196 - 197 - ### Manual Testing Performed 198 - 199 - ✅ App starts with profile migration (saw "Migrated existing dev profile directory") 200 - ✅ Dev build uses "dev" profile (verified in logs) 201 - ✅ Dev and production can run simultaneously (single-instance skip works) 202 - 203 - ### Pending Manual Tests 204 - 205 - - [ ] Create new profile via Settings UI 206 - - [ ] Switch profiles (app restart) 207 - - [ ] Delete profile 208 - - [ ] Enable sync for a profile 209 - - [ ] Sync to different server profiles 210 - - [ ] Verify data isolation between profiles 211 - 212 - ### Automated Tests 213 - 214 - Existing tests already use profile-based isolation: 215 - - `getTestProfile()` generates unique test profiles 216 - - `PROFILE` env var passed to test instances 217 - - Tests should work without modification 218 - 219 - ## Files Modified/Created 220 - 221 - ### Server 222 - - `backend/server/users.js` - Added profiles table and CRUD 223 - - `backend/server/db.js` - Profile-aware connection pooling 224 - - `backend/server/index.js` - Profile API endpoints + migration 225 - 226 - ### Desktop 227 - - **NEW**: `backend/electron/profiles.ts` - Profile management module 228 - - `backend/electron/entry.ts` - Profile initialization and selection 229 - - `backend/electron/ipc.ts` - Profile IPC handlers 230 - - `backend/electron/sync.ts` - Per-profile sync config 231 - - `app/settings/settings.js` - Profiles UI section 232 - - `preload.js` - Profiles API exposure 233 - 234 - ### Documentation 235 - - **NEW**: `docs/profiles.md` - Comprehensive profiles documentation 236 - - `DEVELOPMENT.md` - Updated profile management section 237 - - `docs/sync.md` - Updated with per-profile sync 238 - - **NEW**: `PROFILES-IMPLEMENTATION-SUMMARY.md` - This file 239 - 240 - ## Known Limitations 241 - 242 - 1. **Profile switching requires app restart** - Electron limitation 243 - 2. **No profile encryption** - Data stored in plaintext 244 - 3. **No profile export/import** - Manual directory copy required 245 - 4. **Mobile support pending** - Desktop-only for now 246 - 5. **API keys stored in plaintext** - In local SQLite file 247 - 248 - ## Security Considerations 249 - 250 - - Profile isolation is filesystem-based 251 - - No encryption at rest 252 - - API keys visible in profiles.db 253 - - Single-instance lock skipped for dev profiles (intentional for development) 254 - - Chromium sessions isolated per profile (cookies, cache separated) 255 - 256 - ## Scripts and Build Configs 257 - 258 - ### No Changes Required 259 - 260 - Existing scripts work correctly: 261 - - `yarn start` → Uses dev profile automatically 262 - - `yarn package:install` → Packaged build uses default profile 263 - - Tests use unique profiles per test case 264 - - `PROFILE` env var still works for testing 265 - 266 - ### Future Script Considerations 267 - 268 - Potential additions: 269 - - `yarn profile:list` - List all profiles 270 - - `yarn profile:create <name>` - Create profile from CLI 271 - - `yarn profile:export <slug>` - Export profile data 272 - - `yarn profile:import <slug> <path>` - Import profile data 273 - 274 - ## Deployment Impact 275 - 276 - ### Server Deployment 277 - 278 - **Zero downtime, backward compatible:** 279 - 1. Migrations run automatically on first request 280 - 2. Old API calls (without profile param) work (defaults to "default") 281 - 3. Old clients continue working 282 - 4. New clients can use profiles 283 - 284 - ### Desktop Deployment 285 - 286 - **Zero disruption:** 287 - 1. Migration runs on first launch 288 - 2. Existing data preserved in "default" profile 289 - 3. Users can continue using default profile 290 - 4. Profile switching is opt-in 291 - 292 - ## Future Enhancements 293 - 294 - 1. **Profile import/export** - Backup and restore functionality 295 - 2. **Profile templates** - Pre-configured profiles for different use cases 296 - 3. **Profile encryption** - At-rest encryption for sensitive data 297 - 4. **Profile-level theme settings** - Different themes per profile 298 - 5. **Mobile support** - Extend to Tauri mobile 299 - 6. **Profile backup automation** - Scheduled backups per profile 300 - 7. **Profile-specific extension configs** - Different extensions per profile 301 - 302 - ## Questions Answered 303 - 304 - ### Q: How do Peek profiles relate to Chromium profiles? 305 - 306 - **A: They are nested, not the same.** 307 - 308 - - **Peek Profile** = High-level data workspace (items, tags, sync config) 309 - - **Chromium Profile** = Low-level browser session data (cookies, cache) 310 - 311 - Each Peek profile automatically gets its own Chromium session directory. This provides complete isolation including browser state. Users manage Peek profiles; Chromium profiles are an implementation detail. 312 - 313 - ### Q: Can dev and production run simultaneously? 314 - 315 - **A: Yes, by design.** 316 - 317 - Development builds: 318 - - Always use "dev" profile (isolated from production) 319 - - Skip single-instance lock 320 - - Can run alongside production instance safely 321 - 322 - ### Q: What happens to existing data? 323 - 324 - **A: Automatically migrated, zero data loss.** 325 - 326 - - Existing `default/` → "Default" profile 327 - - Existing `dev/` → "Development" profile 328 - - Data stays in same location, just tracked in profiles.db 329 - 330 - ## Commits 331 - 332 - 1. `feat(server): add profiles support to users.js` 333 - 2. `feat(server): add profile-aware connection pooling to db.js` 334 - 3. `feat(server): add profile endpoints and migration to index.js` 335 - 4. `feat(client): create profiles.ts module for profile management` 336 - 5. `feat(client): add profile IPC handlers to ipc.ts` 337 - 6. `feat(client): update entry.ts to initialize profiles` 338 - 7. `feat(sync): update sync.ts for per-profile configuration` 339 - 8. `feat(settings): add profiles section to settings UI` 340 - 9. `feat(frontend): add profiles API to preload` 341 - 10. `fix(profiles): enforce dev profile isolation from production` 342 - 11. `docs: add comprehensive profiles documentation` 343 - 344 - ## Status 345 - 346 - ✅ **Implementation Complete** 347 - ✅ **Documentation Complete** 348 - ⏳ **Manual Testing In Progress** 349 - 350 - All code is committed and ready for testing.
+14 -2
app/settings/settings.js
··· 1678 1678 const radio = document.createElement('input'); 1679 1679 radio.type = 'radio'; 1680 1680 radio.name = 'profile-switch'; 1681 - radio.checked = currentProfile && profile.id === currentProfile.id; 1681 + const isCurrentProfile = currentProfile && profile.id === currentProfile.id; 1682 + radio.checked = isCurrentProfile; 1682 1683 radio.style.cursor = 'pointer'; 1683 1684 radio.addEventListener('change', () => { 1684 1685 if (radio.checked) { 1685 - if (confirm(`Switch to ${profile.name}? The app will restart.`)) { 1686 + console.log(`[profiles] Switching to profile: ${profile.name} (slug: ${profile.slug})`); 1687 + 1688 + // Double check we're not already on this profile 1689 + if (isCurrentProfile) { 1690 + console.log('[profiles] Already on this profile, skipping switch'); 1691 + return; 1692 + } 1693 + 1694 + if (confirm(`Switch to "${profile.name}"?\n\nThe app will restart to apply the change.`)) { 1695 + console.log('[profiles] User confirmed switch'); 1686 1696 api.profiles.switch(profile.slug).then(result => { 1687 1697 if (!result.success) { 1698 + console.error('[profiles] Switch failed:', result.error); 1688 1699 alert(`Failed to switch profile: ${result.error}`); 1689 1700 radio.checked = false; 1690 1701 } 1691 1702 }); 1692 1703 } else { 1704 + console.log('[profiles] User cancelled switch'); 1693 1705 radio.checked = false; 1694 1706 } 1695 1707 }
+13
backend/electron/ipc.ts
··· 2270 2270 // Switch to a different profile (causes app restart) 2271 2271 ipcMain.handle('profiles:switch', async (_ev, data: { slug: string }) => { 2272 2272 try { 2273 + DEBUG && console.log(`[ipc:profiles] Switch requested to profile: ${data.slug}`); 2274 + 2275 + const currentProfile = getActiveProfile(); 2276 + DEBUG && console.log(`[ipc:profiles] Current profile: ${currentProfile.slug}`); 2277 + 2278 + // Check if already on this profile 2279 + if (currentProfile.slug === data.slug) { 2280 + DEBUG && console.log('[ipc:profiles] Already on requested profile, no-op'); 2281 + return { success: true, message: 'Already on this profile' }; 2282 + } 2283 + 2273 2284 const profile = getProfile(data.slug); 2274 2285 if (!profile) { 2275 2286 return { success: false, error: 'Profile not found' }; 2276 2287 } 2277 2288 2278 2289 // Set as active profile 2290 + DEBUG && console.log(`[ipc:profiles] Setting active profile to: ${data.slug}`); 2279 2291 setActiveProfile(data.slug); 2280 2292 2293 + DEBUG && console.log('[ipc:profiles] Relaunching app...'); 2281 2294 // Relaunch the app with the new profile 2282 2295 // The app will pick up the new active profile from profiles.db on restart 2283 2296 app.relaunch();
+130
docs/profiles.md
··· 330 330 # Verify items go to correct server profile 331 331 ``` 332 332 333 + ## Implementation Notes 334 + 335 + ### Files Modified/Created 336 + 337 + **Server:** 338 + - `backend/server/users.js` - Added profiles table and CRUD functions 339 + - `backend/server/db.js` - Profile-aware connection pooling (userId:profileSlug) 340 + - `backend/server/index.js` - Profile API endpoints + data migration 341 + 342 + **Desktop:** 343 + - `backend/electron/profiles.ts` - Profile management module (NEW) 344 + - `backend/electron/entry.ts` - Profile initialization and selection 345 + - `backend/electron/ipc.ts` - Profile IPC handlers 346 + - `backend/electron/sync.ts` - Per-profile sync configuration 347 + - `app/settings/settings.js` - Profiles UI section 348 + - `preload.js` - Profiles API exposure 349 + 350 + **Documentation:** 351 + - `docs/profiles.md` - This file 352 + - `DEVELOPMENT.md` - Updated profile management section 353 + - `docs/sync.md` - Updated with per-profile sync 354 + 355 + ### Design Principles 356 + 357 + **1. Production/Dev Isolation** 358 + 359 + The most critical rule: development builds NEVER touch production data. 360 + 361 + ```typescript 362 + // Profile selection logic (entry.ts) 363 + if (PROFILE_ENV_VAR) { 364 + PROFILE = PROFILE_ENV_VAR; // Explicit override 365 + } else if (!app.isPackaged || isDevPackagedBuild()) { 366 + PROFILE = 'dev'; // Development ALWAYS uses dev 367 + } else { 368 + PROFILE = getActiveProfile().slug; // Production uses profiles.db 369 + } 370 + ``` 371 + 372 + **2. Single-Instance Lock** 373 + 374 + ```typescript 375 + export function requestSingleInstance(): boolean { 376 + // Skip lock for dev/test profiles 377 + if (isDevProfile() || isTestProfile()) { 378 + return true; // Allow multiple instances 379 + } 380 + 381 + // Production profiles enforce single instance 382 + return app.requestSingleInstanceLock(); 383 + } 384 + ``` 385 + 386 + This allows dev and production to run simultaneously without conflicts. 387 + 388 + **3. Automatic Migration** 389 + 390 + On first launch with profiles support: 391 + 1. `initProfilesDb()` creates `profiles.db` if missing 392 + 2. `migrateExistingProfiles()` detects existing profile directories 393 + 3. Existing data preserved in original locations 394 + 4. Profile records created in profiles.db 395 + 396 + No user action required, zero data loss. 397 + 398 + **4. Nested Profile Relationship** 399 + 400 + Peek profiles contain Chromium profiles: 401 + - Peek manages the outer profile (application data) 402 + - Electron manages the inner profile (browser session) 403 + - User never directly interacts with Chromium profiles 404 + - Isolation is automatic and transparent 405 + 406 + ### Backward Compatibility 407 + 408 + - `PROFILE` env var still works (overrides all logic) 409 + - Existing profile directories detected and migrated 410 + - Server API endpoints default to `profile=default` 411 + - Old clients continue working without changes 412 + 413 + ### Migration Strategy 414 + 415 + **Server Migration:** 416 + - `migrateUserDataToProfiles()` runs automatically on first request 417 + - Moves `data/{userId}/peek.db` → `data/{userId}/profiles/default/datastore.sqlite` 418 + - Creates "default" profile record in system.db 419 + - Idempotent (safe to run multiple times) 420 + 421 + **Desktop Migration:** 422 + - Runs on startup before profile selection 423 + - Detects `{userData}/default/` and `{userData}/dev/` directories 424 + - Creates profile records if they don't exist 425 + - Never deletes or moves data 426 + 427 + ### Commits 428 + 429 + Implementation was completed in 11 atomic commits: 430 + 431 + 1. `feat(server): add profiles support to users.js` 432 + 2. `feat(server): add profile-aware connection pooling to db.js` 433 + 3. `feat(server): add profile endpoints and migration to index.js` 434 + 4. `feat(client): create profiles.ts module for profile management` 435 + 5. `feat(client): add profile IPC handlers to ipc.ts` 436 + 6. `feat(client): update entry.ts to initialize profiles` 437 + 7. `feat(sync): update sync.ts for per-profile configuration` 438 + 8. `feat(settings): add profiles section to settings UI` 439 + 9. `feat(frontend): add profiles API to preload` 440 + 10. `fix(profiles): enforce dev profile isolation from production` 441 + 11. `docs: add comprehensive profiles documentation` 442 + 443 + ### Testing Considerations 444 + 445 + **Automated Tests:** 446 + - Existing tests already use profile isolation via `getTestProfile()` 447 + - Each test gets unique profile: `test-{name}-{timestamp}` 448 + - No test modifications required 449 + 450 + **Scripts:** 451 + - `yarn start` → Automatically uses dev profile 452 + - `yarn package:install` → Packaged build uses default profile 453 + - `PROFILE` env var for testing overrides 454 + 455 + ### Known Issues 456 + 457 + 1. **Profile switching requires app restart** - Electron limitation (userData path set once) 458 + 2. **No prompt for already-selected profile** - Radio button behavior (change event only fires on actual change) 459 + 3. **API keys stored in plaintext** - In local SQLite file (consider encryption for future) 460 + 333 461 ## Future Enhancements 334 462 335 463 - Profile import/export functionality ··· 339 467 - Mobile profile support (Tauri) 340 468 - Profile encryption at rest 341 469 - Profile-specific extension configurations 470 + - Profile migration between machines 471 + - Cloud backup of profiles