From 42fa2426aa3e9f72ec25fc564db33705eb63a0be Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Tue, 29 Sep 2026 13:46:16 +0200 Subject: [PATCH] Let the browse page refresh itself A dropdown below the title reloads the HTML view every 1, 5, 10, 30 or 60 seconds, for watching metrics move without pressing reload. The choice lives in the browser's localStorage, so it survives the reload and a click into a child. While an input has the focus the reload waits a round, so a value being typed is not thrown away. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- .../CanopyBrowseHtmlHandlerTest.class.st | 24 ++++++++++++++++++- .../CanopyBrowseHtmlHandler.class.st | 11 +++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index deb330e..7436b2a 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,8 @@ while it is read is left out, and the rest of the scrape continues. `CanopyBrowseHtmlHandler` wraps it. Requests without `text/html` in `Accept` are passed through unchanged. A browser gets a table per branch, with links into child branches and an editable field -with a Save button per leaf. +with a Save button per leaf. A dropdown below the title reloads the page every 1 to 60 seconds; the +choice is kept in the browser, and no reload happens while an input has the focus. ```smalltalk Canopy registerSystemMetrics. diff --git a/source/Canopy-Core-Tests/CanopyBrowseHtmlHandlerTest.class.st b/source/Canopy-Core-Tests/CanopyBrowseHtmlHandlerTest.class.st index 3aefa81..4779ee0 100644 --- a/source/Canopy-Core-Tests/CanopyBrowseHtmlHandlerTest.class.st +++ b/source/Canopy-Core-Tests/CanopyBrowseHtmlHandlerTest.class.st @@ -81,11 +81,33 @@ CanopyBrowseHtmlHandlerTest >> testHandleRequestEscapesHtmlSpecialCharactersInVa ] ensure: [ Canopy reset ] ] +{ #category : 'tests' } +CanopyBrowseHtmlHandlerTest >> testHandleRequestRendersAutoRefreshControlOnBranchPage [ + "The dropdown sits right below the title, so it is the same place on every page." + | body | + [ self registerBrowseTestDomain. + body := (CanopyBrowseHtmlHandler new handleRequest: (self requestFor: #GET url: 'http://localhost/browseTest' accept: 'text/html' body: nil)) entity contents. + self assert: (body includesSubstring: '

') + ] ensure: [ Canopy reset ] +] + { #category : 'tests' } CanopyBrowseHtmlHandlerTest >> testHandleRequestRendersHtmlPageForLeafWhenAcceptIsHtml [ [ self registerBrowseTestDomain. self assert: (CanopyBrowseHtmlHandler new handleRequest: (self requestFor: #GET url: 'http://localhost/browseTest/a' accept: 'text/html' body: nil)) entity contents - equals: '

/browseTest/a

Value: "x"

' + equals: '

/browseTest/a

' , CanopyBrowseHtmlHandler new refreshControl , '

Value: "x"

' ] ensure: [ Canopy reset ] ] diff --git a/source/Canopy-Core/CanopyBrowseHtmlHandler.class.st b/source/Canopy-Core/CanopyBrowseHtmlHandler.class.st index 757e84d..c3c5337 100644 --- a/source/Canopy-Core/CanopyBrowseHtmlHandler.class.st +++ b/source/Canopy-Core/CanopyBrowseHtmlHandler.class.st @@ -45,6 +45,7 @@ CanopyBrowseHtmlHandler >> branchPageFor: node path: pathString [ ^ String streamContents: [ :s | s nextPutAll: ''. s nextPutAll: '

/'; nextPutAll: (self htmlEscape: pathString); nextPutAll: '

'. + s nextPutAll: self refreshControl. s nextPutAll: ''. node keysAndValuesDo: [ :key :child | | href | @@ -97,12 +98,22 @@ CanopyBrowseHtmlHandler >> pageFor: node path: pathString [ ^ String streamContents: [ :s | s nextPutAll: ''. s nextPutAll: '

/'; nextPutAll: (self htmlEscape: pathString); nextPutAll: '

'. + s nextPutAll: self refreshControl. s nextPutAll: '

Value: '; nextPutAll: (self htmlEscape: currentJson); nextPutAll: '

'. s nextPutAll: ''. s nextPutAll: ''. s nextPutAll: '' ] ] +{ #category : 'private' } +CanopyBrowseHtmlHandler >> refreshControl [ + "A dropdown that reloads the page every n seconds, for watching metrics move. The choice is + kept in the browser's localStorage, so it survives the reload and a click into a child - and + stays with the viewer rather than with the tree. While an input has the focus the reload + waits for the next round, or a value being typed would be thrown away." + ^ '

' +] + { #category : 'accessing' } CanopyBrowseHtmlHandler >> restHandler [ ^ restHandler ifNil: [ restHandler := CanopyBrowseHandler new ]