OS Apps: Glance and TimeTagger

Published: Jun 4, 2024 by Isaac Johnson

I had two bookmarked apps I wanted to check out that originally were mentioned on MariusHosting. The first is a landing page that is YAML driven called Glance. Glance is easy to install and configure - probably the best I’ve tried thus far.

The other app I wanted to check out is TimeTagger which can be hosted in Docker or Kubernetes and is used to just keep a log of time spent on things.

Let’s start with Glance.

Glance

I came across Glance from a MariusHosting post.

The Github page points out we can just launch with docker using

docker run -d -p 8080:8080 \
  -v ./glance.yml:/app/glance.yml \
  -v /etc/timezone:/etc/timezone:ro \
  -v /etc/localtime:/etc/localtime:ro \
  glanceapp/glance

A little bit buried in the docs is an example glance.yml file: https://github.com/glanceapp/glance/blob/main/docs/configuration.md#preconfigured-page

pages:
  - name: Home
    columns:
      - size: small
        widgets:
          - type: calendar

          - type: rss
            limit: 10
            collapse-after: 3
            cache: 3h
            feeds:
              - url: https://ciechanow.ski/atom.xml
              - url: https://www.joshwcomeau.com/rss.xml
                title: Josh Comeau
              - url: https://samwho.dev/rss.xml
              - url: https://awesomekling.github.io/feed.xml
              - url: https://ishadeed.com/feed.xml
                title: Ahmad Shadeed

          - type: twitch-channels
            channels:
              - theprimeagen
              - cohhcarnage
              - christitustech
              - blurbs
              - asmongold
              - jembawls

      - size: full
        widgets:
          - type: hacker-news

          - type: videos
            channels:
              - UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling
              - UCv6J_jJa8GJqFwQNgNrMuww # ServeTheHome
              - UCOk-gHyjcWZNj3Br4oxwh0A # Techno Tim

          - type: reddit
            subreddit: selfhosted

      - size: small
        widgets:
          - type: weather
            location: London, United Kingdom

          - type: stocks
            stocks:
              - symbol: SPY
                name: S&P 500
              - symbol: BTC-USD
                name: Bitcoin
              - symbol: NVDA
                name: NVIDIA
              - symbol: AAPL
                name: Apple
              - symbol: MSFT
                name: Microsoft
              - symbol: GOOGL
                name: Google
              - symbol: AMD
                name: AMD
              - symbol: RDDT
                name: Reddit

Since I like to run with Kubernetes, I’ll convert this over to a manifest with a deployment and service

apiVersion: v1
kind: ConfigMap
metadata:
  name: glanceconfig
data:
  glance.yml: |
    pages:
      - name: Home
        columns:
          - size: small
            widgets:
              - type: calendar

              - type: rss
                limit: 10
                collapse-after: 3
                cache: 3h
                feeds:
                  - url: https://ciechanow.ski/atom.xml
                  - url: https://www.joshwcomeau.com/rss.xml
                    title: Josh Comeau
                  - url: https://samwho.dev/rss.xml
                  - url: https://awesomekling.github.io/feed.xml
                  - url: https://ishadeed.com/feed.xml
                    title: Ahmad Shadeed

              - type: twitch-channels
                channels:
                  - theprimeagen
                  - cohhcarnage
                  - christitustech
                  - blurbs
                  - asmongold
                  - jembawls

          - size: full
            widgets:
              - type: hacker-news

              - type: videos
                channels:
                  - UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling
                  - UCv6J_jJa8GJqFwQNgNrMuww # ServeTheHome
                  - UCOk-gHyjcWZNj3Br4oxwh0A # Techno Tim

              - type: reddit
                subreddit: selfhosted

          - size: small
            widgets:
              - type: weather
                location: London, United Kingdom

              - type: stocks
                stocks:
                  - symbol: SPY
                    name: S&P 500
                  - symbol: BTC-USD
                    name: Bitcoin
                  - symbol: NVDA
                    name: NVIDIA
                  - symbol: AAPL
                    name: Apple
                  - symbol: MSFT
                    name: Microsoft
                  - symbol: GOOGL
                    name: Google
                  - symbol: AMD
                    name: AMD
                  - symbol: RDDT
                    name: Reddit
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: glance-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: glance
  template:
    metadata:
      labels:
        app: glance
    spec:
      containers:
        - name: glance
          image: glanceapp/glance
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: config-volume
              mountPath: /app/glance.yml
              subPath: glance.yml
      volumes:
        - name: config-volume
          configMap:
            name: glanceconfig
---
apiVersion: v1
kind: Service
metadata:
  name: glance-service
spec:
  type: ClusterIP
  selector:
    app: glance
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

Which I applied

$ kubectl apply -f ./manifest.yml
Warning: resource configmaps/glanceconfig is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
configmap/glanceconfig configured
deployment.apps/glance-deployment created
service/glance-service created

Seems it doesn’t like that CM

$ kubectl get pods -l app=glance
NAME                                 READY   STATUS    RESTARTS   AGE
glance-deployment-5547d948ff-69h6q   1/1     Running   0          46s

I can now port-forward to test

$ kubectl port-forward svc/glance-service 8888:80
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
Handling connection for 8888
Handling connection for 8888

/content/images/2024/06/glance-01.png

One thing I realized is that when I change the Configmap, it doesn’t live update. This is because we mount as a volume on the pod. What that means is that if you update your glance.yml, you need to also bounce the pod to make it take effect

builder@LuiGi:~/Workspaces/glance$ kubectl apply -f ./manifest.yml
configmap/glanceconfig configured
deployment.apps/glance-deployment unchanged
service/glance-service unchanged
builder@LuiGi:~/Workspaces/glance$ kubectl port-forward svc/glance-service 8888:80
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
Handling connection for 8888
Handling connection for 8888
^Cbuilder@LuiGi:~/Workspaces/glance$ kubectl get pods -l app=glance
NAME                                 READY   STATUS    RESTARTS   AGE
glance-deployment-5547d948ff-69h6q   1/1     Running   0          11m
builder@LuiGi:~/Workspaces/glance$ kubectl delete pod glance-deployment-5547d948ff-69h6q
pod "glance-deployment-5547d948ff-69h6q" deleted

or a bit easier

builder@LuiGi:~/Workspaces/glance$ kubectl apply -f ./manifest.yml
configmap/glanceconfig configured
deployment.apps/glance-deployment unchanged
service/glance-service unchanged
builder@LuiGi:~/Workspaces/glance$ kubectl delete pod -l app=glance
pod "glance-deployment-5547d948ff-s8cqk" deleted

Once I got it configured to my tastes

/content/images/2024/06/glance-02.png

I figured I might as well expose it, perhaps I can use it as a landing page. At the very least, I can keep an eye on my RSS feed

I need to make an A Record

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n glance
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "2c7054da-e087-4fa9-95f1-3b8da2b08d20",
  "fqdn": "glance.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/glance",
  "name": "glance",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

Then I can create an ingress to that same service

builder@LuiGi:~/Workspaces/glance$ cat glance.tpk.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  name: glance-ingress
spec:
  rules:
  - host: glance.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: glance-service
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - glance.tpk.pw
    secretName: glance-tls
builder@LuiGi:~/Workspaces/glance$ kubectl apply -f ./glance.tpk.yaml
ingress.networking.k8s.io/glance-ingress created

Once I see the cert is satisified

builder@LuiGi:~/Workspaces/glance$ kubectl get cert glance-tls
NAME         READY   SECRET       AGE
glance-tls   False   glance-tls   38s
builder@LuiGi:~/Workspaces/glance$ kubectl get cert glance-tls
NAME         READY   SECRET       AGE
glance-tls   False   glance-tls   63s
glance-tls   False   glance-tls   78s
builder@LuiGi:~/Workspaces/glance$ kubectl get cert glance-tls
NAME         READY   SECRET       AGE
glance-tls   True    glance-tls   84s

I can test https://glance.tpk.pw/

/content/images/2024/06/glance-03.png

It’s really quite easy to update. Here we can see me changing the theme and making it live:

Time Tagger

Before I install, I’ll need to create a BCrypt password. Luckily the author made it easy by creating a utility page we can use

/content/images/2024/06/timetagger-01.png

The same Docker compose shows the quick way to launch in Docker

version: "3"
services:
  timetagger:
    image: ghcr.io/almarklein/timetagger
    ports:
      - "80:80"
    volumes:
      - ./_timetagger:/root/_timetagger
    environment:
      - TIMETAGGER_BIND=0.0.0.0:80
      - TIMETAGGER_DATADIR=/root/_timetagger
      - TIMETAGGER_LOG_LEVEL=info
      - TIMETAGGER_CREDENTIALS=test:$$2a$$08$$zMsjPEGdXHzsu0N/felcbuWrffsH4.4ocDWY5oijsZ0cbwSiLNA8.  # test:test

I’ll turn this into a Kuberntes YAML manifest and launch it

$ cat timetagger.manifest.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: timetagger-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: timetagger-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: timetagger
  template:
    metadata:
      labels:
        app: timetagger
    spec:
      containers:
        - name: timetagger
          image: ghcr.io/almarklein/timetagger
          ports:
            - containerPort: 80
          env:
            - name: TIMETAGGER_BIND
              value: "0.0.0.0:80"
            - name: TIMETAGGER_DATADIR
              value: "/root/_timetagger"
            - name: TIMETAGGER_LOG_LEVEL
              value: "info"
            - name: TIMETAGGER_CREDENTIALS
              value: "test:$$2a$$08$$zMsjPEGdXHzsu0N/felcbuWrffsH4.4ocDWY5oijsZ0cbwSiLNA8."
          volumeMounts:
            - name: timetagger-volume
              mountPath: /root/_timetagger
      volumes:
        - name: timetagger-volume
          persistentVolumeClaim:
            claimName: timetagger-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: timetagger-service
spec:
  selector:
    app: timetagger
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

$ kubectl apply -f ./timetagger.manifest.yaml
persistentvolumeclaim/timetagger-pvc created
deployment.apps/timetagger-deployment created
service/timetagger-service created

I can see it’s running

$ kubectl get pods -l app=timetagger
NAME                                     READY   STATUS    RESTARTS   AGE
timetagger-deployment-75c5df6d65-bhht8   1/1     Running   0          65s

I’ll skip to creating the Ingress now as well

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n timetagger
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "a72c0b63-971a-4a1b-a808-98fb5e374592",
  "fqdn": "timetagger.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/timetagger",
  "name": "timetagger",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

Then, as before, fire off the Ingress

$ cat timetagger.tpk.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  name: timetagger-ingress
spec:
  rules:
  - host: timetagger.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: timetagger-service
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - timetagger.tpk.pw
    secretName: timetagger-tls

$ kubectl apply -f ./timetagger.tpk.yaml
ingress.networking.k8s.io/timetagger-ingress created

Once the cert was ready, I could test

builder@LuiGi:~/Workspaces/glance$ kubectl get cert timetagger-tls
NAME             READY   SECRET           AGE
timetagger-tls   False   timetagger-tls   53s
builder@LuiGi:~/Workspaces/glance$ kubectl get cert timetagger-tls
NAME             READY   SECRET           AGE
timetagger-tls   True    timetagger-tls   2m7s

/content/images/2024/06/timetagger-02.png

I can then press record to start tracking time

/content/images/2024/06/timetagger-03.png

I’ll give it a note

/content/images/2024/06/timetagger-04.png

I can see it tracking my time

/content/images/2024/06/timetagger-05.png

What is pretty nice is that the time is not Javascript or browser dependent.

I was able to login to a different browser and see it maintained the time tag

/content/images/2024/06/timetagger-06.png

How fault tolerant is it? Does it use the PVC?

Let’s rotate the pod and check

builder@LuiGi:~/Workspaces/glance$ kubectl get pods -l app=timetagger
NAME                                     READY   STATUS    RESTARTS   AGE
timetagger-deployment-75c5df6d65-bhht8   1/1     Running   0          11m
builder@LuiGi:~/Workspaces/glance$ kubectl delete pods -l app=timetagger
pod "timetagger-deployment-75c5df6d65-bhht8" deleted
builder@LuiGi:~/Workspaces/glance$ kubectl get pods -l app=timetagger
NAME                                     READY   STATUS    RESTARTS   AGE
timetagger-deployment-75c5df6d65-6k2qf   1/1     Running   0          8s

It still is recording!

/content/images/2024/06/timetagger-07.png

I’ll try another radical change and switch users. I moved from a test user to builder with a different password

$ kubectl apply -f ./timetagger.manifest.yaml
persistentvolumeclaim/timetagger-pvc unchanged
deployment.apps/timetagger-deployment configured
service/timetagger-service unchanged

Test no longer works

/content/images/2024/06/timetagger-08.png

Forgetting to stop

I started it and then planned to stop that Blogging activity that night. I forgot.

Coming back two days later, that entry seems to have disappeared

/content/images/2024/06/timetagger-09.png

I’ll try adding some time today. So today I spent my first 45m cleaning some 3d prints, restarting a project and just general 3d Printing right-brain work

/content/images/2024/06/timetagger-10.png

I noticed that if I start a new activity, the old one is done. Perhaps that makes sense, tracking simultaneous work might be a challenge. That said, it’s good to know the behavior.

/content/images/2024/06/timetagger-11.png

I can always restart a task by looking at recent entries. It doesn’t create a duplicate, just adds more time (continues the clock)

/content/images/2024/06/timetagger-12.png

One can also grab a start or stop time in the page and click-hold-drag to move it to a new start/stop time

/content/images/2024/06/timetagger-22.png

Reporting

Clicking “Report” will show recent entries and allow us to group by tags or description

/content/images/2024/06/timetagger-13.png

For instance, a report of this week grouped by tag

/content/images/2024/06/timetagger-14.png

If I copy table, this is what ends up in the clipboard:

1:09	Total					
0:44	#3dprint					
0:44	2024-05-24	06:00	06:43	3D Print Cleanup, Starts, #3dprint
0:25	#blog					
0:25	2024-05-24	06:43	07:08	Blogging, timetagger #blog
0:00	2024-05-24	07:08	07:08	Blogging, timetagger #blog

which pastes just fine into Excel

/content/images/2024/06/timetagger-15.png

The PDF has a nice breakdown by time

/content/images/2024/06/timetagger-16.png

And, of course, the CSV imports into Excel jsut fine as well

/content/images/2024/06/timetagger-17.png

We can use the zoom buttons to zoom in and out with ease

/content/images/2024/06/timetagger-18.png

A little bit of usage:

Backups

We can copy our data out, however it is just to the clipboard

/content/images/2024/06/timetagger-20.png

I thought the import was nice in that it automatically eliminates existing duplicate entries

/content/images/2024/06/timetagger-21.png

Summary

I hope you found at least one of these apps to be of use. I enjoyed Glance as a very easy to launch and configure landing page. It was easy to turn it into a Kubernetes app using a configmap for the config YAML.

Timetagger is a very fast little app. I’m surprised how feature rich it is. I plan to try and use it for a few side projects. I only wish I could have multiple users (or identities).

Glance Timetagger Opensource Containers Kubernetes

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes