experiments in a post-browser web
10
fork

Configure Feed

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

fix(datastore): backfill stuck "Loading..." titles to hostname on boot

The entities feature now falls back to URL hostname when pageMetadata.title
is empty, but only on the fly for items the user revisits. Existing
rows whose page never reported a real title would otherwise stay
title-less indefinitely, leaving cards rendering as blank or "Loading...".

Adds a one-shot migration that scans URL items with title='Loading...'
or empty/null and rewrites them to the URL hostname (matching the same
fallback the entities flow uses). Idempotent via the standard migrations
table.

+88
+44
backend/electron/datastore.ts
··· 383 383 migrateContextHistory(); 384 384 migrateEntityType(); 385 385 migrateEventType(); 386 + migrateLoadingTitles(); 386 387 migrateStandalonePriceEntities(); 387 388 migrateStandaloneDateEntities(); 388 389 migrateNoisyDateEntities(); ··· 1653 1654 } finally { 1654 1655 db.pragma('foreign_keys = ON'); 1655 1656 } 1657 + } 1658 + 1659 + /** 1660 + * One-time backfill: replace stuck "Loading..." titles on URL items with 1661 + * the URL hostname. Captures items whose page never reported a real title 1662 + * (fetch failed, page omitted <title>, etc.); the entities flow now does 1663 + * this on the fly for new items, but already-stored rows would otherwise 1664 + * stay title-less indefinitely. 1665 + */ 1666 + function migrateLoadingTitles(): void { 1667 + if (!db) return; 1668 + 1669 + const MIGRATION_ID = 'backfill_loading_titles_v1'; 1670 + 1671 + const migrationRecord = db.prepare('SELECT * FROM migrations WHERE id = ?').get(MIGRATION_ID) as { status: string } | undefined; 1672 + if (migrationRecord && migrationRecord.status === 'complete') return; 1673 + 1674 + try { 1675 + const stuck = db.prepare( 1676 + `SELECT id, content, domain FROM items 1677 + WHERE type = 'url' AND deletedAt = 0 AND (title = 'Loading...' OR title IS NULL OR title = '')` 1678 + ).all() as { id: string; content: string | null; domain: string | null }[]; 1679 + 1680 + let backfilled = 0; 1681 + const update = db.prepare('UPDATE items SET title = ?, updatedAt = ? WHERE id = ?'); 1682 + const updatedAt = Date.now(); 1683 + for (const row of stuck) { 1684 + let hostname = row.domain || ''; 1685 + if (!hostname && row.content) { 1686 + try { hostname = new URL(row.content).hostname; } catch { /* skip */ } 1687 + } 1688 + if (hostname) { 1689 + update.run(hostname, updatedAt, row.id); 1690 + backfilled++; 1691 + } 1692 + } 1693 + 1694 + DEBUG && console.log('main', `Loading-title backfill: scanned ${stuck.length}, backfilled ${backfilled}`); 1695 + } catch (error) { 1696 + DEBUG && console.log('main', `Loading-title backfill error:`, (error as Error).message); 1697 + } 1698 + 1699 + db.prepare('INSERT OR REPLACE INTO migrations (id, status, completedAt) VALUES (?, ?, ?)').run(MIGRATION_ID, 'complete', Date.now()); 1656 1700 } 1657 1701 1658 1702 /**
+44
tests/desktop/loading-title-backfill.spec.ts
··· 1 + /** 2 + * Regression coverage: items with stuck title="Loading..." get rewritten 3 + * to the URL hostname on the fly via the entities flow (already landed) 4 + * AND via the one-shot migration on app boot for already-stored rows. 5 + */ 6 + import { test, expect, DesktopApp } from '../fixtures/desktop-app'; 7 + import { Page } from '@playwright/test'; 8 + import { createPerDescribeApp } from '../helpers/test-app'; 9 + 10 + test.describe('Loading title residual @desktop', () => { 11 + let app: DesktopApp; 12 + let bgWindow: Page; 13 + 14 + test.beforeAll(async () => { 15 + ({ app, bgWindow } = await createPerDescribeApp('loading-title-backfill')); 16 + }); 17 + 18 + test.afterAll(async () => { 19 + if (app) await app.close(); 20 + }); 21 + 22 + test('updateItemTitle replaces stuck Loading... with a real title', async () => { 23 + // Seed a URL item with the literal "Loading..." title. 24 + const seededUrl = `https://load-test-${Date.now()}.example.com/page`; 25 + const add = await bgWindow.evaluate(async (url: string) => { 26 + return await (window as any).app.datastore.addItem('url', { 27 + content: url, 28 + title: 'Loading...', 29 + }); 30 + }, seededUrl); 31 + expect(add.success).toBe(true); 32 + const itemId = add.data?.id; 33 + 34 + const replaced = await bgWindow.evaluate(async ({ url }: { url: string }) => { 35 + return await (window as any).app.datastore.updateItemTitle(url, 'Real Title'); 36 + }, { url: seededUrl }); 37 + expect(replaced.success).toBe(true); 38 + 39 + const fetched = await bgWindow.evaluate(async (id: string) => { 40 + return await (window as any).app.datastore.getItem(id); 41 + }, itemId); 42 + expect(fetched.data?.title).toBe('Real Title'); 43 + }); 44 + });