experiments in a post-browser web
10
fork

Configure Feed

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

fix(tile-ipc): implement settings/theme stubs, document datastore

+76 -22
+46 -21
backend/electron/tile-ipc.ts
··· 16 16 * Every handler validates the capability token before executing. 17 17 */ 18 18 19 - import { ipcMain, BrowserWindow, net, dialog, webContents } from 'electron'; 19 + import { ipcMain, BrowserWindow, net, dialog, webContents, nativeTheme } from 'electron'; 20 20 import fs from 'node:fs'; 21 21 import path from 'node:path'; 22 22 ··· 30 30 import { resolveCapabilities, validateTileManifest, detectManifestVersion } from './tile-manifest.js'; 31 31 import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 32 32 import { invokeWindowOpen } from './ipc.js'; 33 + import { getActiveThemeId } from './protocol.js'; 33 34 34 35 // ─── Helpers ───────────────────────────────────────────────────────── 35 36 ··· 576 577 return { error: 'Theme capability not granted' }; 577 578 } 578 579 579 - // Return basic theme info — actual theme state comes from main 580 + // Return real theme state by querying the active theme + color scheme. 581 + const themeId = getActiveThemeId(); 582 + const isDark = nativeTheme.shouldUseDarkColors; 580 583 return { 581 - // The real implementation would query the active theme 582 - activeTheme: 'peek', 584 + activeTheme: themeId, 585 + themeId, 586 + isDark, 587 + effectiveScheme: isDark ? 'dark' : 'light', 583 588 }; 584 589 }); 585 590 ··· 594 599 return { error: 'Settings capability not granted' }; 595 600 } 596 601 597 - // Scoped to the tile's own settings namespace 598 - // The real implementation would use the datastore 599 - return { value: null }; 602 + // Scoped to the tile's own feature_settings namespace. 603 + try { 604 + const db = getDb(); 605 + const row = db.prepare( 606 + 'SELECT value FROM feature_settings WHERE featureId = ? AND key = ?' 607 + ).get(grant.tileId, args.key) as { value: string } | undefined; 608 + if (!row) return { value: null }; 609 + try { 610 + return { value: JSON.parse(row.value) }; 611 + } catch { 612 + return { value: row.value }; 613 + } 614 + } catch (error) { 615 + const message = error instanceof Error ? error.message : String(error); 616 + return { error: message }; 617 + } 600 618 }); 601 619 602 620 ipcMain.handle('tile:settings:set', async (_event, args: { ··· 609 627 return { error: 'Settings capability not granted' }; 610 628 } 611 629 612 - // Scoped to the tile's own settings namespace 613 - return { success: true }; 630 + // Scoped to the tile's own feature_settings namespace. 631 + try { 632 + const db = getDb(); 633 + const jsonValue = JSON.stringify(args.value); 634 + db.prepare(` 635 + INSERT OR REPLACE INTO feature_settings (id, featureId, key, value, updatedAt) 636 + VALUES (?, ?, ?, ?, ?) 637 + `).run(`${grant.tileId}_${args.key}`, grant.tileId, args.key, jsonValue, Date.now()); 638 + return { success: true }; 639 + } catch (error) { 640 + const message = error instanceof Error ? error.message : String(error); 641 + return { success: false, error: message }; 642 + } 614 643 }); 615 644 616 645 // ── Datastore ── 617 646 647 + // NOTE: tile:datastore:* are not yet implemented. No feature currently uses them. 648 + // When a feature needs scoped table access, wire these to getDb() with per-tile 649 + // row filtering (e.g., WHERE source = grant.tileId) and a SQL allow-list per table. 650 + // Returning explicit errors avoids silent data loss from pretend-successful writes. 651 + 618 652 ipcMain.handle('tile:datastore:get', async (_event, args: { 619 653 token: string; 620 654 table: string; ··· 624 658 if (!grant || !hasCapability(grant, 'datastore')) { 625 659 return { error: 'Datastore capability not granted' }; 626 660 } 627 - 628 661 if (!hasDatastoreTable(grant, args.table)) { 629 662 return { error: `Table not allowed: ${args.table}` }; 630 663 } 631 - 632 - // The real implementation would use getDb() 633 - return { value: null }; 664 + return { error: 'tile:datastore:get not yet implemented — use pubsub + ipc to reach core datastore' }; 634 665 }); 635 666 636 667 ipcMain.handle('tile:datastore:set', async (_event, args: { ··· 643 674 if (!grant || !hasCapability(grant, 'datastore')) { 644 675 return { error: 'Datastore capability not granted' }; 645 676 } 646 - 647 677 if (!hasDatastoreTable(grant, args.table)) { 648 678 return { error: `Table not allowed: ${args.table}` }; 649 679 } 650 - 651 - // The real implementation would use getDb() 652 - return { success: true }; 680 + return { success: false, error: 'tile:datastore:set not yet implemented — no feature currently requires it' }; 653 681 }); 654 682 655 683 ipcMain.handle('tile:datastore:query', async (_event, args: { ··· 661 689 if (!grant || !hasCapability(grant, 'datastore')) { 662 690 return { error: 'Datastore capability not granted' }; 663 691 } 664 - 665 692 if (!hasDatastoreTable(grant, args.table)) { 666 693 return { error: `Table not allowed: ${args.table}` }; 667 694 } 668 - 669 - // The real implementation would use getDb() 670 - return { rows: [] }; 695 + return { rows: [], error: 'tile:datastore:query not yet implemented — no feature currently requires it' }; 671 696 }); 672 697 673 698 // ── Feature Registry ──
+30 -1
backend/electron/tile-preload.ts
··· 356 356 357 357 api.settings = { 358 358 /** 359 - * Get a setting value 359 + * Get a setting value (returns raw value, no wrapper) 360 360 */ 361 361 get: (key: string) => { 362 362 if (!tokenValid) return Promise.reject(new Error('Not initialized')); ··· 376 376 key, 377 377 value, 378 378 }); 379 + }, 380 + 381 + /** 382 + * v1 compatibility alias — get returns {success, data} shape 383 + * matching legacy preload's settings.getKey(). 384 + */ 385 + getKey: async (key: string) => { 386 + if (!tokenValid) return { success: false, error: 'Not initialized' }; 387 + const result = await ipcRenderer.invoke('tile:settings:get', { 388 + token: tileToken, 389 + key, 390 + }); 391 + if (result.error) return { success: false, error: result.error }; 392 + return { success: true, data: result.value }; 393 + }, 394 + 395 + /** 396 + * v1 compatibility alias — set returns {success, error?} shape 397 + * matching legacy preload's settings.setKey(). 398 + */ 399 + setKey: async (key: string, value: unknown) => { 400 + if (!tokenValid) return { success: false, error: 'Not initialized' }; 401 + const result = await ipcRenderer.invoke('tile:settings:set', { 402 + token: tileToken, 403 + key, 404 + value, 405 + }); 406 + if (result.error) return { success: false, error: result.error }; 407 + return { success: true }; 379 408 }, 380 409 }; 381 410