Releases: glanceapp/glance
v0.8.3
New
- Added the ability to disable the theme picker via
disable-picker: true
undertheme
(thanks @NeoNyaa) - Added
mod
function (modulo) to the custom API widget (thanks @anant-j) - Added
safeHTML
function to the custom API widget which prevents HTML from being escaped (thanks @sudoexec) - Added
.Options.JSON
to the custom API widget which takes any nested option value and turns it into a JSON string (thanks @ralphocdol)
View example
- type: custom-api
options:
request-body:
service: Session
method: login
params:
username: ${USERNAME}
password: ${PASSWORD}
# useful if you need to send JSON in a request body
template: |
...
| withStringBody (.Options.JSON "request-body")
...
- Added the theme key to the document root so that you can apply styles based on the selected theme (thanks @EricKotato)
View example
:root[data-theme="default-dark"] {
.color-primary-if-not-visited {
color: #ff0000;
}
}
:root[data-theme="default-light"] {
.color-primary-if-not-visited {
color: #00ff00;
}
}
:root[data-theme="your-theme-name"] {
.color-primary-if-not-visited {
color: #0000ff;
}
}
Fixes
- Fixed
HTTP_PROXY
andHTTPS_PROXY
environment variables not getting used (thanks @mazzz1y) - Fixed truncated links' hover text having unnecessary whitespace (thanks @ralphocdol)
Visit the release notes for v0.8.0 to learn about all new features
v0.8.2
- Fixed
hide-deskop-navigation
not working (thanks @LZStealth) - Fixed custom favicons not working in Chromium browsers (thanks @AWildLeon)
- Fixed server crash when using
head-widgets
with the releases widget (thanks @aykhans) - Fixed URLs getting escaped when using
video-url-template
in thevideos
widget with thevertical-list
style (thanks @Kamalaja) - Added
startOfDay
andendOfDay
functions to the custom API widget (thanks @ralphocdol)
Visit the release notes for v0.8.0 to learn about all new features
v0.8.1
Fixed an error that prevented the page from loading when hide-desktop-navigation
is set to true
(thanks @Geffreyvanderbos and @Blooym)
Visit the release notes for v0.8.0 to learn about all new features
v0.8.0
Another big update is here! This one includes authentication, a theme picker, new customization options, a to-do widget and lots of improvements to existing widgets. Before we get to details of all that...
Feedback wanted
Deciding what features to add to a project like this is no easy task. Most self-hosted services focus on doing one thing and adding features around that thing, but with a personal dashboard/feed aggregator you can practically display anything, so how do you decide what does and doesn't belong? So far I've been prioritizing feature requests with a high number of upvotes and those I believe fit with the vision of the project, although admittedly Glance has deviated quite a bit from its initial purpose.
As with almost all communities, a minority of people are very vocal about their wants and needs, while the majority are just riding along. Rather than guessing what the community as a whole would benefit from the most moving forward, I thought I could just ask through a short survey.
It's entirely anonymous, should only take about 1 to 3 minutes to complete, and I'd be happy to share the results once there are enough responses. Thank you to those who take the time to fill it out.
Jump to section
- Community widgets
- New logo
- New features
- Widget enhancements
- [Docker containers] Container customization in the config, support for proxies,
category
andformat-container-names
properties - [Custom API] Synchronous API calls and
options
property - [Reddit] Support for application auth
- [RSS] Conditional requests
- [Monitor]
basic-auth
andtimeout
properties - [DNS Stats] Support for Technitium
- [Docker containers] Container customization in the config, support for proxies,
Community widgets
If you weren't aware, there's now a repository for widgets made by the community with 40+ widgets available so far. Most are made using the custom-api
widget, so adding them is as simple as copying them into your glance.yml
. It's a great place to find new widgets, and if you have a widget that you think others would benefit from, please consider sharing it there. Thank you to everyone who has contributed!
New logo
Big thanks to @Tom607 for offering help with the logo and going over dozens of different iterations!
I believe it does a great job at conveying the widgety layout of Glance, while also being simple and clean, as is the rest of the design language.
New features
Nested config includes and new include syntax
Up until now, you could only use the !include
directive in your glance.yml
file, but not within any of the included files. This has been changed so that you can includes files from within included files as many times as you need, meaning you can have a separate file for each page, a separate file for each column, and even a separate file for each widget if you wanted to:
glance.yml
├── home.yml
│ ├── column1.yml
│ │ ├── widget1.yml
│ │ └── widget2.yml
│ └── column2.yml
│ ├── widget3.yml
│ └── widget4.yml
In addition, the !include
directive can now instead be written as $include
. The reason for this change is that it turns out !include
, with the way it was used in Glance, is not actually valid YAML (whoops), meaning it makes YAML validators and syntax highlighters angry. The new $include
syntax is valid YAML and will be the preferred syntax moving forward. You can also add a -
at the beginning of a line when including array items, since that's the expected syntax for YAML arrays.
Before:
theme:
!include: theme.yml
pages:
!include: home.yml
!include: homelab.yml
!include: startpage.yml
Now:
theme:
$include: theme.yml
pages:
- $include: home.yml
- $include: homelab.yml
- $include: startpage.yml
Using Docker secrets within the config
Along with environment variables, you can now use Docker secrets in your config file using the following syntax:
token: ${secret:github_token} # Reads the contents of /run/secrets/github_token
token: ${GITHUB_TOKEN} # Environment variable
(thanks @gomeology)
Theme picker
You can now define multiple theme presets and switch between them in the UI, no more config changes needed to change the theme!
View config
theme:
presets:
neon-focus:
background-color: 255 14 6
primary-color: 156 50 65
negative-color: 342 65 65
contrast-multiplier: 0.9
text-saturation-multiplier: 0.8
gruvbox-dark:
background-color: 0 0 16
primary-color: 43 59 81
positive-color: 61 66 44
negative-color: 6 96 59
catppuccin-macchiato:
background-color: 232 23 18
contrast-multiplier: 1.2
primary-color: 220 83 75
positive-color: 105 48 72
negative-color: 351 74 73
peachy:
light: true
background-color: 28 40 77
primary-color: 155 100 20
negative-color: 0 100 60
contrast-multiplier: 1.1
text-saturation-multiplier: 0.5
Glance will include its own default dark and light themes in the list of presets, which you can override by using the default-dark
and default-light
keys.
(thanks @hecht-a)
Authentication
You can now add authentication via username and password:
auth:
secret-key: # this must be set to a random value generated using the secret:make CLI command
users:
admin:
password: ${ADMIN_PASSWORD}
While there is support for multiple users, you cannot customize the dashboard per user at this time, so all users will see the same dashboard.
To generate the secret key, use the secret:make
CLI command:
docker run --rm glanceapp/glance secret:make
Or with the binary:
/opt/glance/glance secret:make
If you do not want to store plain passwords in environment variables or in files, you can hash your password and provide it via the password-hash
property instead:
auth:
secret-key: # this must be set to a random value generated using the secret:make CLI command
users:
admin:
password-hash: $2y$10$...
To hash your password, use the password:hash <password>
CLI command:
docker run --rm glanceapp/glance password:hash mypasswordishunter2
Or with the binary:
/opt/glance/glance password:hash mypasswordishunter2
To-do widget
A fairly simple to-do widget with the ability to add, edit, reorder and delete tasks:
- type: to-do
There are a number of keyboard shortcuts available that make it easier to use, which you can find over at the config docs.
The tasks are stored in the browser's local storage, so they won't be available on all devices. If you want to add multiple todo widgets, you must specify a unique id
for each one:
- type: to-do
title: Work tasks
id: work
- type: to-do
title: Personal tasks
id: personal
(thanks @AMS003010)
Hiding widget headers
A fairly simple addition, but one that's been requested quite a few times - you can now hide widget headers by setting the hide-header
property to true
:
widgets:
- type: calendar
hide-header: true
If you do this for all widgets, you get something like this:
Note that you cannot hide the header of the group widget since it's used to switch between each widget.
Head widgets
You can now define "head widgets" on each page, which are basically widgets that sit above all columns and take up the full width. Combined with the hide-header
property, you get something like this:
View config
pages:
- name: Home
head-widgets:
- type: markets
hide-header: true
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
name: Bitcoin
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
columns:
- size: small
widgets:
- type: calendar
- size: full
widgets:
...
v0.7.13
New
- [Custom API] Added
parseLocalTime
function, which works likeparseTime
, however in the absence of a timezone it uses the local timezone instead of UTC. - [Search widget] Added
target
property which is used when opening the search results in a new tab (thanks @Jacksaur)
Fixes
- [Server stats widget] Fixed being unable to retrieve temperature sensors in some instances (thanks @anxdpanic)
- [Reddit widget] Fixed the URL not being correctly escaped when using
request-url-template
(thanks @yurigiu)
v0.7.12
Fixed server crash in some configurations when a widget is provided with a single resource to fetch instead of multiple
v0.7.11
What's changed
- [RSS widget] Duplicate post links will now be filtered out (thanks @HtFilia)
- [Custom API widget] Added
replaceMatches
andunique
functions (thanks @ralphocdol) - Added
mountpoint:info
CLI command - Added more details to the output of
sensors:print
CLI command
Fixes
- [Twitch channels widget] Fixed live channels with 0 viewers getting incorrectly sorted lower than offline channels
- [Server stats widget] Potential fix for missing mountpoint stats
- Fixed the Yahoo finance test request in the
diagnose
CLI command
v0.7.10
What's Changed
- Slightly increased the scale of everything on mobile to make things easier to tap on
- Added
sensors:print
CLI command that lists all temperature sensors that Glance has access to - Bumped versions of dependencies
New Contributors
- @diceroll123 made their first contribution in #528
- @FranklyFuzzy made their first contribution in #499
- @daot made their first contribution in #486
- @Panonim made their first contribution in #553
Full Changelog: v0.7.9...v0.7.10
v0.7.9
Another small release that's primarily focused around custom-api
widget improvements.
Changed
You no longer need to convert numbers to float before doing math operations with them. When adding two integers together, the result will be an integer. Adding two floats together or an integer and a float will result in a float.
Before:
{{ (add (.JSON.Int "foo" | toFloat) (.JSON.Int "bar" | toFloat)) | toInt }}
Now:
{{ add (.JSON.Int "foo") (.JSON.Int "bar") }}
(this is consistent for all math operations, not just addition)
New
skip-json-validation
and .JSONLines
- type: custom-api
url: https://api.example.com
skip-json-validation: true
Some API's return newline separated JSON objects instead of a single JSON array. This is not valid JSON, so the custom-api
widget will fail to parse it. You can now set skip-json-validation
to true
to skip the validation step and parse the response as a list of JSON objects within your template.
{"name": "Steve", "age": 30}
{"name": "Alex", "age": 25}
{"name": "John", "age": 35}
You can then access the JSON objects using .JSONLines
:
{{ range .JSONLines }}
{{ .String "name" }}
{{ end }}
New functions
concat
{{ concat "foo" "bar" "baz" }}
will return foobarbaz
.
(thanks @ralphocdol)
now
Returns the current time as a time.Time
object.
{{ now }}
{{ now.Hour }}:{{ now.Minute }}:{{ now.Second }}
{{ now.Unix }}
2025-03-29 17:20:18.4905224 +0000 GMT m=+376.954385401
17:20:18
1743268818
This can also be used to convert time to your current timezone:
{{ $parsedTime := .JSON.String "date_created" | parseTime "rfc3339" }}
{{ $created := $parsedTime.In now.Location }}
{{ $created.Hour }}:{{ $created.Minute }}:{{ $created.Second }}
(thanks @not-first & @ralphocdol)
offsetNow
Returns the current time offset by a given amount:
{{ offsetNow "1h" }}
{{ now }}
{{ offsetNow "-1h" }}
2025-03-29 18:44:04.2719241 +0000 GMT m=+4178.324981601
2025-03-29 17:44:04.2719241 +0000 GMT m=+578.324981601
2025-03-29 16:44:04.2719241 +0000 GMT m=-3021.675018399
This can be used to check if something happened within a certain time frame:
{{ $created := .JSON.String "date_created" | parseTime "rfc3339" }}
{{ if ($created.After (offsetNow "-1h")) }}
Created less than 1 hour ago
{{ end }}
Widget-Title-URL
header for extension widget
The extension widget can now return a Widget-Title-URL
header to set the link of the widget's title if it has not already been set by the user.
(thanks @not-first)
Fixes
- The markets widget will now properly display prices of markets where the price is less than 0.01 (thanks @nsdont)
v0.7.8
The custom-api
widget has received a lot of positive feedback since its introduction in v0.7.0. Many people have made and shared their own widgets over at the new community-widgets
repository as well as the Discord server. This release includes many improvements based on feedback from the community that will make it even more capable and easier to use.
New
Insecure requests
You can now allow insecure requests (those to APIs behind a self-signed certificate) via a allow-insecure
property:
- type: custom-api
url: https://api.example.com
allow-insecure: true
(thanks @ralphocdol)
Parameters
You can now specify query parameters via a parameters
property:
- type: custom-api
url: https://api.example.com
parameters:
foo: bar
baz: qux
Note
Using the parameters property will override any query parameters specified in the URL.
(thanks @ralphocdol)
Request method and body
You can now specify the request method and body via the method
, body-type
and body
properties:
- type: custom-api
url: https://api.example.com
method: POST
body-type: json
body:
foo: bar
baz: qux
If you set a body
, the method
will automatically be set to POST
and the body-type
will be set to json
, so you don't have to specify them explicitly.
- type: custom-api
url: https://api.example.com
body-type: string
body: |
foo=bar&baz=qux
(thanks @not-first)
Subrequests
You can now make multiple requests in a single custom-api
widget via a subrequests
property:
- type: custom-api
url: https://api.example.com
subrequests:
another-one:
url: https://api.example.com/another-one
and-another-one:
url: https://api.example.com/and-another-one
Subrequests can take all of the same properties as the main request, such as: parameters
, headers
, method
, body-type
, body
and allow-insecure
.
To access the JSON of a subrequest, you can use .Subrequest "key"
:
<p>{{ (.Subrequest "another-one").JSON.String "foo" }}</p>
This can get cumbersome to write if you need to reference the subrequest in multiple places, so you can instead assign it to a variable:
{{ $anotherOne := .Subrequest "another-one" }}
<p>{{ $anotherOne.JSON.String "foo" }}</p>
<p>{{ $anotherOne.JSON.Int "bar" }}</p>
You can also access the response as you would on the main request:
{{ $andAnotherOne := .Subrequest "and-another-one" }}
<p>{{ $andAnotherOne.Response.StatusCode }}</p>
(thanks @ralphocdol)
New functions
trimPrefix
{"foo": "bazbar"}
<p>{{ .JSON.String "foo" | trimPrefix "baz" }}</p>
<p>bar</p>
trimSuffix
{"foo": "barbaz"}
<p>{{ .JSON.String "foo" | trimSuffix "baz" }}</p>
<p>bar</p>
trimSpace
{"foo": " bar "}
<p>{{ .JSON.String "foo" | trimSpace }}</p>
<p>bar</p>
replaceAll
{"foo": "barbazbar"}
<p>{{ .JSON.String "foo" | replaceAll "baz" "bar" }}</p>
<p>barbarbar</p>
findMatch
{"foo": "bar-123456-baz"}
<p>{{ .JSON.String "foo" | findMatch "\\d+" }}</p>
The pattern is a regular expression, although note that backslashes need to be escaped, so \d
in a normal regular expression would be \\d
here.
<p>123456</p>
findSubmatch
{"foo": "bar-unknown-value"}
<p>{{ .JSON.String "foo" | findSubmatch "bar-(.*)" }}</p>
The pattern is a regular expression, and only the first submatch is returned.
<p>unknown-value</p>
parseTime
{"foo": "2021-01-02T15:04:05Z"}
{{ $parsedTime := .JSON.String "foo" | parseTime "rfc3339" }}
<p>Year: {{ $parsedTime.Year }}</p>
<p>Month: {{ $parsedTime.Month }}</p>
<p>Day: {{ $parsedTime.Day }}</p>
<p>Hour: {{ $parsedTime.Hour }}</p>
<p>Minute: {{ $parsedTime.Minute }}</p>
<p>Second: {{ $parsedTime.Second }}</p>
Other accepted time formats are unix
, rfc3339nano
, datetime
, dateonly
or a custom format using Go's date formatting. The returned object is Go's time.Time
.
<p>Year: 2021</p>
<p>Month: January</p>
<p>Day: 2</p>
<p>Hour: 15</p>
<p>Minute: 4</p>
<p>Second: 5</p>
toRelativeTime
{"foo": "2021-01-02T15:04:05Z"}
<p {{ .JSON.String "foo" | parseTime "rfc3339" | toRelativeTime }}></p>
Note
The return value of this function must be placed within a tag's attributes, not within its content.
<p data-dynamic-relative-time="1609602245"></p>
This will automatically be converted to 1d
, 2h
, etc. on the client side.
parseRelativeTime
This is just a shorthand for parsing time and converting it to relative time. Instead of:
<p {{ .JSON.String "foo" | parseTime "rfc3339" | toRelativeTime }}></p>
You can simply do:
<p {{ .JSON.String "foo" | parseRelativeTime "rfc3339" }}></p>
sortByString
, sortByInt
, sortByFloat
, sortByTime
{"students": [
{"name": "Bob", "age": 20},
{"name": "Alice", "age": 30},
{"name": "Charlie", "age": 10}
]}
{{ range .JSON.Array "students" | sortByString "name" "asc" }}
<p>{{ .String "name" }}</p>
{{ end }}
<p>Alice</p>
<p>Bob</p>
<p>Charlie</p>
{{ range .JSON.Array "students" | sortByInt "age" "desc" }}
<p>{{ .Int "age" }}</p>
{{ end }}
<p>30</p>
<p>20</p>
<p>10</p>
{"students": [
{"name": "Bob", "gpa": 3.5},
{"name": "Alice", "gpa": 4.0},
{"name": "Charlie", "gpa": 2.0}
]}
{{ range .JSON.Array "students" | sortByFloat "gpa" "asc" }}
<p>{{ .Float "gpa" }}</p>
{{ end }}
<p>2</p>
<p>3.5</p>
<p>4</p>
{"students": [
{"name": "Bob", "dob": "2000-01-01"},
{"name": "Alice", "dob": "1990-01-01"},
{"name": "Charlie", "dob": "2010-01-01"}
]}
{{ range .JSON.Array "students" | sortByTime "dob" "dateonly" "asc" }}
<p>{{ .String "name" }}</p>
{{ end }}
Here, dateonly
is the same format that you would use with parseTime
.
<p>Alice</p>
<p>Bob</p>
<p>Charlie</p>
Other additions
Extension widget headers
property
You can now specify headers in the extension
widget:
- type: extension
headers:
Authorization: Bearer token
Fixes
- Fixed being unable to parse an empty response body in the
custom-api
widget - Fixed always overriding query parameters in the
extension
widget, they will now only be overridden if theparameters
property is set