detached e7ca981

refactor(types): align homepage services with generated models

commit
e7ca9814d74e728946394ec3b476c18da6fdfb4e
author
bamen · · 2025-09-17 05:17 UTC
parent
8775e93
signature
unsigned
5 files · +46 -37
M gui/src/lib/types.ts +10 -0
@@ -1,3 +1,5 @@
1+import type { HistoryEntry, SearchShortcut } from '../types/generated.js';
2+
13 // Global window API types for browser integration
24 declare global {
35 interface Window {
@@ -12,6 +14,14 @@ declare global {
1214 __dumber_initial_theme?: string;
1315 __dumber_setTheme?: (theme: 'light' | 'dark') => void;
1416
17+ // History callbacks
18+ __dumber_history_recent?: (data: HistoryEntry[]) => void;
19+ __dumber_history_error?: (error: string) => void;
20+
21+ // Shortcuts callbacks
22+ __dumber_shortcuts_loaded?: (data: Record<string, SearchShortcut>) => void;
23+ __dumber_shortcuts_error?: (error: string) => void;
24+
1525 // WebKit message handler
1626 webkit?: {
1727 messageHandlers?: {
@@ -1,3 +1,5 @@
1+import type { HistoryEntry, SearchShortcut } from '../types/generated.js';
2+
1 // Global window API types for browser integration3 // Global window API types for browser integration
2 declare global {4 declare global {
3 interface Window {5 interface Window {
@@ -12,6 +14,14 @@ declare global {
12 __dumber_initial_theme?: string;14 __dumber_initial_theme?: string;
13 __dumber_setTheme?: (theme: 'light' | 'dark') => void;15 __dumber_setTheme?: (theme: 'light' | 'dark') => void;
14 16
17+ // History callbacks
18+ __dumber_history_recent?: (data: HistoryEntry[]) => void;
19+ __dumber_history_error?: (error: string) => void;
20+
21+ // Shortcuts callbacks
22+ __dumber_shortcuts_loaded?: (data: Record<string, SearchShortcut>) => void;
23+ __dumber_shortcuts_error?: (error: string) => void;
24+
15 // WebKit message handler25 // WebKit message handler
16 webkit?: {26 webkit?: {
17 messageHandlers?: {27 messageHandlers?: {
M gui/src/services/app.ts +17 -21
@@ -1,15 +1,8 @@
1-type HistoryEntry = {
2- id: number;
3- url: string;
4- title: string;
5- visit_count: number;
6- last_visited: string | null;
7- created_at: string | null;
8-}
1+import type { HistoryEntry, SearchShortcut } from '../types/generated.js';
92
103 export class AppService {
114 private history: HistoryEntry[] = [];
12- private shortcuts: Record<string, any> = {};
5+ private shortcuts: Record<string, SearchShortcut> = {};
136
147 constructor() {}
158
@@ -29,18 +22,18 @@ export class AppService {
2922 // Use message bridge instead of fetch
3023 return new Promise((resolve, reject) => {
3124 // Set up response handler
32- (window as any).__dumber_history_recent = (data: HistoryEntry[]) => {
25+ window.__dumber_history_recent = (data: HistoryEntry[]) => {
3326 this.history = Array.isArray(data) ? data : [];
3427 resolve();
3528 };
3629
37- (window as any).__dumber_history_error = (error: string) => {
30+ window.__dumber_history_error = (error: string) => {
3831 console.error('Failed to load history:', error);
3932 reject(new Error(error));
4033 };
4134
4235 // Send message to Go backend
43- const bridge = (window as any).webkit?.messageHandlers?.dumber;
36+ const bridge = window.webkit?.messageHandlers?.dumber;
4437 if (bridge && typeof bridge.postMessage === 'function') {
4538 bridge.postMessage(JSON.stringify({
4639 type: 'history_recent',
@@ -88,12 +81,15 @@ export class AppService {
8881 const cfg = await fetch('/api/config').then(r => r.json());
8982 const raw = cfg?.search_shortcuts || {};
9083 // Normalize field casing from backend (supports URL/Description and url/description)
91- const normalized: Record<string, { description: string; url: string }> = {};
84+ const normalized: Record<string, SearchShortcut> = {};
9285 for (const [key, value] of Object.entries(raw)) {
93- const v: any = value as any;
86+ const v = value as Record<string, unknown>;
9487 normalized[key] = {
95- description: v.description ?? v.Description ?? '',
96- url: v.url ?? v.URL ?? ''
88+ id: 0,
89+ shortcut: key,
90+ url_template: (v.url ?? v.URL ?? '') as string,
91+ description: (v.description ?? v.Description ?? '') as string,
92+ created_at: null
9793 };
9894 }
9995 this.shortcuts = normalized;
@@ -101,10 +97,10 @@ export class AppService {
10197 console.error('Failed to load shortcuts:', error);
10298 // Mock shortcuts for development/fallback
10399 this.shortcuts = {
104- 'g': { description: 'Google Search', url: 'https://google.com/search?q={query}' },
105- 'gh': { description: 'GitHub Search', url: 'https://github.com/search?q={query}' },
106- 'so': { description: 'Stack Overflow', url: 'https://stackoverflow.com/search?q={query}' },
107- 'w': { description: 'Wikipedia', url: 'https://en.wikipedia.org/wiki/Special:Search?search={query}' }
100+ 'g': { id: 0, shortcut: 'g', url_template: 'https://google.com/search?q={query}', description: 'Google Search', created_at: null },
101+ 'gh': { id: 0, shortcut: 'gh', url_template: 'https://github.com/search?q={query}', description: 'GitHub Search', created_at: null },
102+ 'so': { id: 0, shortcut: 'so', url_template: 'https://stackoverflow.com/search?q={query}', description: 'Stack Overflow', created_at: null },
103+ 'w': { id: 0, shortcut: 'w', url_template: 'https://en.wikipedia.org/wiki/Special:Search?search={query}', description: 'Wikipedia', created_at: null }
108104 };
109105 }
110106 }
@@ -113,7 +109,7 @@ export class AppService {
113109 return this.history;
114110 }
115111
116- getShortcuts(): Record<string, any> {
112+ getShortcuts(): Record<string, SearchShortcut> {
117113 return this.shortcuts;
118114 }
119115
@@ -1,15 +1,8 @@
1-type HistoryEntry = {1+import type { HistoryEntry, SearchShortcut } from '../types/generated.js';
2- id: number;
3- url: string;
4- title: string;
5- visit_count: number;
6- last_visited: string | null;
7- created_at: string | null;
8-}
9 2
10 export class AppService {3 export class AppService {
11 private history: HistoryEntry[] = [];4 private history: HistoryEntry[] = [];
12- private shortcuts: Record<string, any> = {};5+ private shortcuts: Record<string, SearchShortcut> = {};
13 6
14 constructor() {}7 constructor() {}
15 8
@@ -29,18 +22,18 @@ export class AppService {
29 // Use message bridge instead of fetch22 // Use message bridge instead of fetch
30 return new Promise((resolve, reject) => {23 return new Promise((resolve, reject) => {
31 // Set up response handler24 // Set up response handler
32- (window as any).__dumber_history_recent = (data: HistoryEntry[]) => {25+ window.__dumber_history_recent = (data: HistoryEntry[]) => {
33 this.history = Array.isArray(data) ? data : [];26 this.history = Array.isArray(data) ? data : [];
34 resolve();27 resolve();
35 };28 };
36 29
37- (window as any).__dumber_history_error = (error: string) => {30+ window.__dumber_history_error = (error: string) => {
38 console.error('Failed to load history:', error);31 console.error('Failed to load history:', error);
39 reject(new Error(error));32 reject(new Error(error));
40 };33 };
41 34
42 // Send message to Go backend35 // Send message to Go backend
43- const bridge = (window as any).webkit?.messageHandlers?.dumber;36+ const bridge = window.webkit?.messageHandlers?.dumber;
44 if (bridge && typeof bridge.postMessage === 'function') {37 if (bridge && typeof bridge.postMessage === 'function') {
45 bridge.postMessage(JSON.stringify({38 bridge.postMessage(JSON.stringify({
46 type: 'history_recent',39 type: 'history_recent',
@@ -88,12 +81,15 @@ export class AppService {
88 const cfg = await fetch('/api/config').then(r => r.json());81 const cfg = await fetch('/api/config').then(r => r.json());
89 const raw = cfg?.search_shortcuts || {};82 const raw = cfg?.search_shortcuts || {};
90 // Normalize field casing from backend (supports URL/Description and url/description)83 // Normalize field casing from backend (supports URL/Description and url/description)
91- const normalized: Record<string, { description: string; url: string }> = {};84+ const normalized: Record<string, SearchShortcut> = {};
92 for (const [key, value] of Object.entries(raw)) {85 for (const [key, value] of Object.entries(raw)) {
93- const v: any = value as any;86+ const v = value as Record<string, unknown>;
94 normalized[key] = {87 normalized[key] = {
95- description: v.description ?? v.Description ?? '',88+ id: 0,
96- url: v.url ?? v.URL ?? ''89+ shortcut: key,
90+ url_template: (v.url ?? v.URL ?? '') as string,
91+ description: (v.description ?? v.Description ?? '') as string,
92+ created_at: null
97 };93 };
98 }94 }
99 this.shortcuts = normalized;95 this.shortcuts = normalized;
@@ -101,10 +97,10 @@ export class AppService {
101 console.error('Failed to load shortcuts:', error);97 console.error('Failed to load shortcuts:', error);
102 // Mock shortcuts for development/fallback98 // Mock shortcuts for development/fallback
103 this.shortcuts = {99 this.shortcuts = {
104- 'g': { description: 'Google Search', url: 'https://google.com/search?q={query}' },100+ 'g': { id: 0, shortcut: 'g', url_template: 'https://google.com/search?q={query}', description: 'Google Search', created_at: null },
105- 'gh': { description: 'GitHub Search', url: 'https://github.com/search?q={query}' },101+ 'gh': { id: 0, shortcut: 'gh', url_template: 'https://github.com/search?q={query}', description: 'GitHub Search', created_at: null },
106- 'so': { description: 'Stack Overflow', url: 'https://stackoverflow.com/search?q={query}' },102+ 'so': { id: 0, shortcut: 'so', url_template: 'https://stackoverflow.com/search?q={query}', description: 'Stack Overflow', created_at: null },
107- 'w': { description: 'Wikipedia', url: 'https://en.wikipedia.org/wiki/Special:Search?search={query}' }103+ 'w': { id: 0, shortcut: 'w', url_template: 'https://en.wikipedia.org/wiki/Special:Search?search={query}', description: 'Wikipedia', created_at: null }
108 };104 };
109 }105 }
110 }106 }
@@ -113,7 +109,7 @@ export class AppService {
113 return this.history;109 return this.history;
114 }110 }
115 111
116- getShortcuts(): Record<string, any> {112+ getShortcuts(): Record<string, SearchShortcut> {
117 return this.shortcuts;113 return this.shortcuts;
118 }114 }
119 115
M gui/src/services/debug-console.ts +4 -4
@@ -4,7 +4,7 @@ export interface DebugLogEntry {
44 source: 'frontend' | 'backend';
55 category: string;
66 message: string;
7- data?: any;
7+ data?: unknown;
88 }
99
1010 export class DebugConsoleService {
@@ -121,7 +121,7 @@ export class DebugConsoleService {
121121 };
122122 }
123123
124- public addLog(level: DebugLogEntry['level'], source: DebugLogEntry['source'], category: string, message: string, data?: any): void {
124+ public addLog(level: DebugLogEntry['level'], source: DebugLogEntry['source'], category: string, message: string, data?: unknown): void {
125125 const entry: DebugLogEntry = {
126126 timestamp: new Date().toISOString().split('T')[1]?.split('.')[0] || '', // HH:MM:SS format
127127 level,
@@ -205,7 +205,7 @@ export class DebugConsoleService {
205205 });
206206 }
207207
208- public logServiceCall(serviceName: string, methodName: string, params: any[], result?: any, error?: Error): void {
208+ public logServiceCall(serviceName: string, methodName: string, params: unknown[], result?: unknown, error?: Error): void {
209209 if (error) {
210210 this.addLog('error', 'frontend', 'service', `${serviceName}.${methodName}() failed`, {
211211 params,
@@ -219,7 +219,7 @@ export class DebugConsoleService {
219219 }
220220 }
221221
222- public logBackendMessage(message: string, data?: any): void {
222+ public logBackendMessage(message: string, data?: unknown): void {
223223 this.addLog('info', 'backend', 'service', message, data);
224224 }
225225
@@ -4,7 +4,7 @@ export interface DebugLogEntry {
4 source: 'frontend' | 'backend';4 source: 'frontend' | 'backend';
5 category: string;5 category: string;
6 message: string;6 message: string;
7- data?: any;7+ data?: unknown;
8 }8 }
9 9
10 export class DebugConsoleService {10 export class DebugConsoleService {
@@ -121,7 +121,7 @@ export class DebugConsoleService {
121 };121 };
122 }122 }
123 123
124- public addLog(level: DebugLogEntry['level'], source: DebugLogEntry['source'], category: string, message: string, data?: any): void {124+ public addLog(level: DebugLogEntry['level'], source: DebugLogEntry['source'], category: string, message: string, data?: unknown): void {
125 const entry: DebugLogEntry = {125 const entry: DebugLogEntry = {
126 timestamp: new Date().toISOString().split('T')[1]?.split('.')[0] || '', // HH:MM:SS format126 timestamp: new Date().toISOString().split('T')[1]?.split('.')[0] || '', // HH:MM:SS format
127 level,127 level,
@@ -205,7 +205,7 @@ export class DebugConsoleService {
205 });205 });
206 }206 }
207 207
208- public logServiceCall(serviceName: string, methodName: string, params: any[], result?: any, error?: Error): void {208+ public logServiceCall(serviceName: string, methodName: string, params: unknown[], result?: unknown, error?: Error): void {
209 if (error) {209 if (error) {
210 this.addLog('error', 'frontend', 'service', `${serviceName}.${methodName}() failed`, {210 this.addLog('error', 'frontend', 'service', `${serviceName}.${methodName}() failed`, {
211 params,211 params,
@@ -219,7 +219,7 @@ export class DebugConsoleService {
219 }219 }
220 }220 }
221 221
222- public logBackendMessage(message: string, data?: any): void {222+ public logBackendMessage(message: string, data?: unknown): void {
223 this.addLog('info', 'backend', 'service', message, data);223 this.addLog('info', 'backend', 'service', message, data);
224 }224 }
225 225
M gui/src/services/notifications.ts +8 -4
@@ -1,5 +1,9 @@
11 import type { NotificationOptions } from '../types/app.js';
22
3+interface TimerElement extends HTMLElement {
4+ timeout?: ReturnType<typeof setTimeout>;
5+}
6+
37 export class NotificationService {
48 private zoomNotification: HTMLElement | null = null;
59 private generalNotification: HTMLElement | null = null;
@@ -63,13 +67,13 @@ export class NotificationService {
6367 element.style.opacity = '1';
6468
6569 // Clear any existing timeout
66- const existingTimeout = (element as any).timeout;
67- if (existingTimeout) {
68- clearTimeout(existingTimeout);
70+ const timerElement = element as TimerElement;
71+ if (timerElement.timeout) {
72+ clearTimeout(timerElement.timeout);
6973 }
7074
7175 // Hide after duration
72- (element as any).timeout = setTimeout(() => {
76+ timerElement.timeout = setTimeout(() => {
7377 element.style.opacity = '0';
7478 }, duration);
7579 }
@@ -1,5 +1,9 @@
1 import type { NotificationOptions } from '../types/app.js';1 import type { NotificationOptions } from '../types/app.js';
2 2
3+interface TimerElement extends HTMLElement {
4+ timeout?: ReturnType<typeof setTimeout>;
5+}
6+
3 export class NotificationService {7 export class NotificationService {
4 private zoomNotification: HTMLElement | null = null;8 private zoomNotification: HTMLElement | null = null;
5 private generalNotification: HTMLElement | null = null;9 private generalNotification: HTMLElement | null = null;
@@ -63,13 +67,13 @@ export class NotificationService {
63 element.style.opacity = '1';67 element.style.opacity = '1';
64 68
65 // Clear any existing timeout69 // Clear any existing timeout
66- const existingTimeout = (element as any).timeout;70+ const timerElement = element as TimerElement;
67- if (existingTimeout) {71+ if (timerElement.timeout) {
68- clearTimeout(existingTimeout);72+ clearTimeout(timerElement.timeout);
69 }73 }
70 74
71 // Hide after duration75 // Hide after duration
72- (element as any).timeout = setTimeout(() => {76+ timerElement.timeout = setTimeout(() => {
73 element.style.opacity = '0';77 element.style.opacity = '0';
74 }, duration);78 }, duration);
75 }79 }
M gui/src/utils/dom.ts +7 -8
@@ -1,5 +1,4 @@
1-type HistoryEntry = { id: number; url: string; title: string };
2-type SearchShortcut = { description: string; url: string };
1+import type { History, Shortcut } from '../types/generated.js';
32
43 export class DOMRenderer {
54 private historyListElement: HTMLElement | null = null;
@@ -10,7 +9,7 @@ export class DOMRenderer {
109 this.shortcutsElement = document.getElementById('shortcuts');
1110 }
1211
13- displayHistory(historyEntries: HistoryEntry[]): void {
12+ displayHistory(historyEntries: History[]): void {
1413 if (!this.historyListElement) return;
1514
1615 if (!historyEntries || historyEntries.length === 0) {
@@ -30,7 +29,7 @@ export class DOMRenderer {
3029 });
3130 }
3231
33- displayShortcuts(shortcutsData: Record<string, SearchShortcut>): void {
32+ displayShortcuts(shortcutsData: Record<string, Shortcut>): void {
3433 if (!this.shortcutsElement) return;
3534
3635 if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
@@ -69,7 +68,7 @@ export class DOMRenderer {
6968 }
7069 }
7170
72- private createHistoryItem(item: HistoryEntry): HTMLElement {
71+ private createHistoryItem(item: History): HTMLElement {
7372 const historyItem = document.createElement('div');
7473 historyItem.className = 'history-item';
7574 const parsed = this.safeParseURL(item.url);
@@ -126,17 +125,17 @@ export class DOMRenderer {
126125 return historyItem;
127126 }
128127
129- private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement {
128+ private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement {
130129 const shortcutEl = document.createElement('div');
131130 shortcutEl.className = 'shortcut';
132131 shortcutEl.innerHTML = `
133132 <div class="shortcut-key">${this.escapeHtml(key)}:</div>
134- <div class="shortcut-desc">${this.escapeHtml(shortcut.description)}</div>
133+ <div class="shortcut-desc">${this.escapeHtml(shortcut.description || 'No description')}</div>
135134 `;
136135
137136 shortcutEl.addEventListener('click', () => {
138137 // Navigate to the base URL of the shortcut (remove %s template parameter)
139- const baseUrl = this.extractBaseUrl(shortcut.url);
138+ const baseUrl = this.extractBaseUrl(shortcut.url_template);
140139 this.navigateToUrl(baseUrl);
141140 });
142141
@@ -1,5 +1,4 @@
1-type HistoryEntry = { id: number; url: string; title: string };1+import type { History, Shortcut } from '../types/generated.js';
2-type SearchShortcut = { description: string; url: string };
3 2
4 export class DOMRenderer {3 export class DOMRenderer {
5 private historyListElement: HTMLElement | null = null;4 private historyListElement: HTMLElement | null = null;
@@ -10,7 +9,7 @@ export class DOMRenderer {
10 this.shortcutsElement = document.getElementById('shortcuts');9 this.shortcutsElement = document.getElementById('shortcuts');
11 }10 }
12 11
13- displayHistory(historyEntries: HistoryEntry[]): void {12+ displayHistory(historyEntries: History[]): void {
14 if (!this.historyListElement) return;13 if (!this.historyListElement) return;
15 14
16 if (!historyEntries || historyEntries.length === 0) {15 if (!historyEntries || historyEntries.length === 0) {
@@ -30,7 +29,7 @@ export class DOMRenderer {
30 });29 });
31 }30 }
32 31
33- displayShortcuts(shortcutsData: Record<string, SearchShortcut>): void {32+ displayShortcuts(shortcutsData: Record<string, Shortcut>): void {
34 if (!this.shortcutsElement) return;33 if (!this.shortcutsElement) return;
35 34
36 if (!shortcutsData || Object.keys(shortcutsData).length === 0) {35 if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
@@ -69,7 +68,7 @@ export class DOMRenderer {
69 }68 }
70 }69 }
71 70
72- private createHistoryItem(item: HistoryEntry): HTMLElement {71+ private createHistoryItem(item: History): HTMLElement {
73 const historyItem = document.createElement('div');72 const historyItem = document.createElement('div');
74 historyItem.className = 'history-item';73 historyItem.className = 'history-item';
75 const parsed = this.safeParseURL(item.url);74 const parsed = this.safeParseURL(item.url);
@@ -126,17 +125,17 @@ export class DOMRenderer {
126 return historyItem;125 return historyItem;
127 }126 }
128 127
129- private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement {128+ private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement {
130 const shortcutEl = document.createElement('div');129 const shortcutEl = document.createElement('div');
131 shortcutEl.className = 'shortcut';130 shortcutEl.className = 'shortcut';
132 shortcutEl.innerHTML = `131 shortcutEl.innerHTML = `
133 <div class="shortcut-key">${this.escapeHtml(key)}:</div>132 <div class="shortcut-key">${this.escapeHtml(key)}:</div>
134- <div class="shortcut-desc">${this.escapeHtml(shortcut.description)}</div>133+ <div class="shortcut-desc">${this.escapeHtml(shortcut.description || 'No description')}</div>
135 `;134 `;
136 135
137 shortcutEl.addEventListener('click', () => {136 shortcutEl.addEventListener('click', () => {
138 // Navigate to the base URL of the shortcut (remove %s template parameter)137 // Navigate to the base URL of the shortcut (remove %s template parameter)
139- const baseUrl = this.extractBaseUrl(shortcut.url);138+ const baseUrl = this.extractBaseUrl(shortcut.url_template);
140 this.navigateToUrl(baseUrl);139 this.navigateToUrl(baseUrl);
141 });140 });
142 141

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close