Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ Server configuration is done through a top level `server` property. Example:
server:
port: 8080
assets-path: /home/user/glance-assets
background-refresh-enabled: true
background-refresh-interval: 15m
```

### Properties
Expand All @@ -268,6 +270,8 @@ server:
| proxied | boolean | no | false |
| base-url | string | no | |
| assets-path | string | no | |
| background-refresh-enabled | boolean | no | false |
| background-refresh-interval | duration | no | 15m |

#### `host`
The address which the server will listen on. Setting it to `localhost` means that only the machine that the server is running on will be able to access the dashboard. By default it will listen on all interfaces.
Expand All @@ -288,6 +292,12 @@ The base URL that Glance is hosted under. No need to specify this unless you're
#### `assets-path`
The path to a directory that will be served by the server under the `/assets/` path. This is handy for widgets like the Monitor where you have to specify an icon URL and you want to self host all the icons rather than pointing to an external source.

#### `background-refresh-enabled`
Whether to enable automatic background refresh of widget data. When enabled, Glance will periodically update all widgets that have outdated cached data in the background, ensuring that your dashboards always show fresh information even when no one is actively viewing them. This significantly improves page load times since widget data is already current when pages are accessed.

#### `background-refresh-interval`
How often to run the background refresh process. Accepts duration values like `5m` (5 minutes), `30s` (30 seconds), `1h` (1 hour), etc. Shorter intervals mean more up-to-date data but may increase resource usage and API calls. Longer intervals reduce resource usage but may result in slightly stale data between refresh cycles.

> [!IMPORTANT]
>
> When installing through docker the path will point to the files inside the container. Don't forget to mount your assets path to the same path inside the container.
Expand Down
Binary file added glance
Binary file not shown.
14 changes: 9 additions & 5 deletions internal/glance/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ const (

type config struct {
Server struct {
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
Proxied bool `yaml:"proxied"`
AssetsPath string `yaml:"assets-path"`
BaseURL string `yaml:"base-url"`
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
Proxied bool `yaml:"proxied"`
AssetsPath string `yaml:"assets-path"`
BaseURL string `yaml:"base-url"`
BackgroundRefreshEnabled bool `yaml:"background-refresh-enabled"`
BackgroundRefreshInterval time.Duration `yaml:"background-refresh-interval"`
} `yaml:"server"`

Auth struct {
Expand Down Expand Up @@ -99,6 +101,8 @@ func newConfigFromYAML(contents []byte) (*config, error) {

config := &config{}
config.Server.Port = 8080
config.Server.BackgroundRefreshEnabled = false
config.Server.BackgroundRefreshInterval = 15 * time.Minute

err = yaml.Unmarshal(contents, config)
if err != nil {
Expand Down
118 changes: 118 additions & 0 deletions internal/glance/glance.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type application struct {
usernameHashToUsername map[string]string
authAttemptsMu sync.Mutex
failedAuthAttempts map[string]*failedAuthAttempt

// Background refresh system
backgroundRefreshTicker *time.Ticker
stopBackground chan struct{}
}

func newApplication(c *config) (*application, error) {
Expand Down Expand Up @@ -269,6 +273,46 @@ func (p *page) updateOutdatedWidgets() {
wg.Wait()
}

// updateWidgetsWithinDuration updates widgets that are outdated or will become outdated within the given duration
func (p *page) updateWidgetsWithinDuration(duration time.Duration) {
now := time.Now()

var wg sync.WaitGroup
context := context.Background()

for w := range p.HeadWidgets {
widget := p.HeadWidgets[w]

if !widget.requiresUpdateWithin(&now, duration) {
continue
}

wg.Add(1)
go func() {
defer wg.Done()
widget.update(context)
}()
}

for c := range p.Columns {
for w := range p.Columns[c].Widgets {
widget := p.Columns[c].Widgets[w]

if !widget.requiresUpdateWithin(&now, duration) {
continue
}

wg.Add(1)
go func() {
defer wg.Done()
widget.update(context)
}()
}
}

wg.Wait()
}

func (a *application) resolveUserDefinedAssetPath(path string) string {
if strings.HasPrefix(path, "/assets/") {
return a.Config.Server.BaseURL + path
Expand Down Expand Up @@ -501,6 +545,11 @@ func (a *application) server() (func() error, func() error) {
absAssetsPath,
)

// Start background refresh if enabled
if a.Config.Server.BackgroundRefreshEnabled {
a.startBackgroundRefresh()
}

if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
Expand All @@ -509,8 +558,77 @@ func (a *application) server() (func() error, func() error) {
}

stop := func() error {
// Stop background refresh first
a.stopBackgroundRefresh()
return server.Close()
}

return start, stop
}

// startBackgroundRefresh starts a goroutine that periodically refreshes outdated widgets
func (a *application) startBackgroundRefresh() {
refreshInterval := a.Config.Server.BackgroundRefreshInterval

a.backgroundRefreshTicker = time.NewTicker(refreshInterval)
a.stopBackground = make(chan struct{})

log.Printf("Starting background widget refresh every %v", refreshInterval)

go func() {
for {
select {
case <-a.backgroundRefreshTicker.C:
a.refreshAllOutdatedWidgets()
case <-a.stopBackground:
return
}
}
}()
}

// stopBackgroundRefresh stops the background refresh goroutine
func (a *application) stopBackgroundRefresh() {
if a.backgroundRefreshTicker != nil {
a.backgroundRefreshTicker.Stop()
}
if a.stopBackground != nil {
close(a.stopBackground)
}
}

// refreshAllOutdatedWidgets proactively updates widgets that are outdated or will become outdated before the next refresh cycle
func (a *application) refreshAllOutdatedWidgets() {
log.Println("Running background widget refresh...")

var wg sync.WaitGroup
refreshInterval := a.Config.Server.BackgroundRefreshInterval

for _, pagePtr := range a.slugToPage {
wg.Add(1)
go func(p *page) {
defer wg.Done()

// Use a timeout for each page to prevent hanging
done := make(chan struct{})
go func() {
defer close(done)
p.mu.Lock()
defer p.mu.Unlock()
// Use predictive refresh: update widgets that will be outdated before next cycle
p.updateWidgetsWithinDuration(refreshInterval)
}()

select {
case <-done:
// Update completed normally
case <-time.After(30 * time.Second):
// Timeout - log warning but continue
log.Printf("Background refresh timeout for page: %s", p.Slug)
}
}(pagePtr)
}

wg.Wait()
log.Println("Background widget refresh completed")
}
16 changes: 16 additions & 0 deletions internal/glance/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ type widget interface {

initialize() error
requiresUpdate(*time.Time) bool
requiresUpdateWithin(*time.Time, time.Duration) bool
setProviders(*widgetProviders)
update(context.Context)
setID(uint64)
Expand Down Expand Up @@ -182,6 +183,21 @@ func (w *widgetBase) requiresUpdate(now *time.Time) bool {
return now.After(w.nextUpdate)
}

// requiresUpdateWithin checks if the widget will require an update within the given duration
func (w *widgetBase) requiresUpdateWithin(now *time.Time, duration time.Duration) bool {
if w.cacheType == cacheTypeInfinite {
return false
}

if w.nextUpdate.IsZero() {
return true
}

// Check if the widget will be outdated before now + duration
futureTime := now.Add(duration)
return futureTime.After(w.nextUpdate) || now.After(w.nextUpdate)
}

func (w *widgetBase) IsWIP() bool {
return w.WIP
}
Expand Down