@@ -136,10 +136,92 @@ async function checkHealth() {
136136 }
137137}
138138
139- // --- Views (stubs, implemented in HB-021 through HB-023) ---
139+ // --- Hook List View ---
140140async function renderHookList ( ) {
141141 show ( 'view-hooks' ) ;
142- $ ( '#view-hooks' ) . innerHTML = '<p class="empty-state">Loading hooks...</p>' ;
142+ const el = $ ( '#view-hooks' ) ;
143+ el . innerHTML = '<p class="empty-state">Loading hooks...</p>' ;
144+
145+ const data = await api . get ( '/api/hooks' ) ;
146+ const hooks = data . hooks || [ ] ;
147+ const origin = location . origin ;
148+
149+ let html = '<div class="toolbar">' ;
150+ html += '<h2 style="margin:0;font-size:16px;">Hooks (' + data . count + ')</h2>' ;
151+ html += '<button class="btn btn-primary" id="btn-create-hook">+ Create Hook</button>' ;
152+ html += '</div>' ;
153+
154+ if ( hooks . length === 0 ) {
155+ html += '<p class="empty-state">No hooks yet. Create one to start capturing webhooks.</p>' ;
156+ } else {
157+ for ( const hook of hooks ) {
158+ const url = origin + hook . url ;
159+ html += '<div class="card card-clickable" data-hook-id="' + escapeHtml ( hook . hook_id ) + '">' ;
160+ html += '<div class="card-header">' ;
161+ html += '<span class="card-title">' + escapeHtml ( hook . name ) + '</span>' ;
162+ html += '<span style="display:flex;gap:6px;flex-shrink:0;">' ;
163+ html += '<button class="btn btn-small btn-copy-url" data-url="' + escapeHtml ( url ) + '" title="Copy URL">copy</button>' ;
164+ html += '<button class="btn btn-small btn-danger btn-delete-hook" data-hook-id="' + escapeHtml ( hook . hook_id ) + '" data-hook-name="' + escapeHtml ( hook . name ) + '" title="Delete">del</button>' ;
165+ html += '</span>' ;
166+ html += '</div>' ;
167+ html += '<div class="card-meta">' ;
168+ html += '<span>' + hook . request_count + ' request' + ( hook . request_count !== 1 ? 's' : '' ) + '</span>' ;
169+ html += '<span class="url-box" style="border:0;padding:0;background:none;font-size:12px;">' + escapeHtml ( url ) + '</span>' ;
170+ html += '<span>' + timeAgo ( hook . created_at ) + '</span>' ;
171+ html += '</div>' ;
172+ html += '</div>' ;
173+ }
174+ }
175+
176+ el . innerHTML = html ;
177+
178+ // Create hook handler
179+ const createBtn = document . getElementById ( 'btn-create-hook' ) ;
180+ if ( createBtn ) {
181+ createBtn . addEventListener ( 'click' , async ( e ) => {
182+ e . stopPropagation ( ) ;
183+ const name = prompt ( 'Hook name (leave blank for auto-generated):' ) ;
184+ if ( name === null ) return ;
185+ try {
186+ const body = name . trim ( ) ? { name : name . trim ( ) } : { } ;
187+ await api . post ( '/api/hooks' , body ) ;
188+ await renderHookList ( ) ;
189+ } catch ( err ) {
190+ toast ( err . error || 'Failed to create hook' , 'error' ) ;
191+ }
192+ } ) ;
193+ }
194+
195+ // Copy URL handlers
196+ el . querySelectorAll ( '.btn-copy-url' ) . forEach ( btn => {
197+ btn . addEventListener ( 'click' , ( e ) => {
198+ e . stopPropagation ( ) ;
199+ copyText ( btn . dataset . url ) ;
200+ } ) ;
201+ } ) ;
202+
203+ // Delete hook handlers
204+ el . querySelectorAll ( '.btn-delete-hook' ) . forEach ( btn => {
205+ btn . addEventListener ( 'click' , async ( e ) => {
206+ e . stopPropagation ( ) ;
207+ const name = btn . dataset . hookName ;
208+ if ( ! confirm ( 'Delete hook "' + name + '"? All captured requests will be lost.' ) ) return ;
209+ try {
210+ await api . del ( '/api/hooks/' + btn . dataset . hookId ) ;
211+ toast ( 'Hook deleted' , 'success' ) ;
212+ await renderHookList ( ) ;
213+ } catch ( err ) {
214+ toast ( err . error || 'Failed to delete hook' , 'error' ) ;
215+ }
216+ } ) ;
217+ } ) ;
218+
219+ // Click card to navigate
220+ el . querySelectorAll ( '.card-clickable' ) . forEach ( card => {
221+ card . addEventListener ( 'click' , ( ) => {
222+ navigate ( '/hooks/' + card . dataset . hookId ) ;
223+ } ) ;
224+ } ) ;
143225}
144226
145227async function renderRequestFeed ( hookId ) {
0 commit comments