GitHub Heatmap
Show off your GitHub contributions right on your AWTRIX.
About this flow
| System | AWTRIX NG Scripts |
|---|---|
| AWTRIX version | AWTRIX NG |
| Topic | Social |
| Built by | Golevka2001 |
| File | fxaGdh4z8w5m.ax · 4.8 KB |
| Icons | — |
| Published | 29 Aug 2026 · updated 1 Sep 2026 |
| Downloads | 72 |
Is this your flow?
If you created this flow, open your saved edit link to add it to your account. You can then manage it whenever you sign in. Adding it to your account replaces the edit link.
Flow description
English | 简体中文
A GitHub contribution heatmap for AWTRIX NG, rendered on the 32×8 LED panel.
AWTRIX app ──▶ Cloudflare Worker ──GraphQL──▶ GitHub API
▲ │
└── 256 pixels ◀──┘ JSON array of 0xRRGGBB ints, 32×8 row-major
Rendering conventions: the panel shows the last 32 weeks; rows 1–7 map Sunday through Saturday, the rightmost column is the current week, and row 0 carries the month marker. With Show Avatar on, the leftmost 8 columns render the user's 8×8 avatar.
1. Create a GitHub token
The worker only reads public data, so a classic personal access token with the read:user scope is all it needs.
- Create one at https://github.com/settings/tokens/new.
- Name it and tick
read:user. - Generate and store it somewhere safe. You will need it in step 2.
2. Deploy the worker
(Recommended) Deploy with the button
Click the button to deploy the worker.
Add a secret named GITHUB_TOKEN and paste the token from step 1.
Manual Deployment
To deploy manually instead, you need to clone the repo and install wrangler:
cd worker
wrangler secret put GITHUB_TOKEN # paste the token from step 1
wrangler deploy
You will see a URL like https://awtrixng-github-heatmap-worker.xxx.workers.dev. Test it with your GitHub username:
curl "https://awtrixng-github-heatmap-worker.xxx.workers.dev/?user=<github-username>"
# [0,934953,27954,934953,...] exactly 256 integers
You can also bind the worker to a custom domain instead of *.workers.dev.
3. (Optional) Protect the worker with Cloudflare Access
Left open, anyone with the URL can query arbitrary accounts and burn your API quota. Access control is recommended.
- Create a new service token:
Zero Trust - Access Controls - Service Credentials - Create Service Token.
Note the Client ID and Client Secret (the secret is shown only once). - Create a new Access policy:
Zero Trust - Access Controls - Policies - Add a Policy.- Include = Service Token - the token created above
- Action = Service Auth
- Add an application:
Zero Trust - Access Controls - Applications - Add an Application - Self-hosted - Workers.- Destinations/Workers/Scope = awtrixng-github-heatmap-worker
- Access Policies = the policy created above
Test the worker again, you will see the Cloudflare Access error page:
curl "https://<worker-url>/?user=<github-username>"
# <!doctype html>
# <html>
# <head>
# <title>Error ・ Cloudflare Access</title>
# ...
You need to pass the service token in the CF-Access-Client-Id and CF-Access-Client-Secret headers:
curl "https://<worker-url>/?user=<github-username>" \
-H "CF-Access-Client-Id: <client-id>" \
-H "CF-Access-Client-Secret: <client-secret>"
# [0,934953,27954,934953,...] exactly 256 integers
4. Install and configure the app
The app is published on AWTRIX Flows — install it from there, or visit the AWTRIX NG web interface, go to Scripts and paste github-heatmap.ax into a new script. Configure it with the following settings:
| Setting | Value |
|---|---|
| Server URL | The worker URL from step 2 |
| Username | GitHub username |
| (Optional) CF Access Client ID / Secret | The service token from step 3. Leave empty if not using Cloudflare Access |
| Rainbow Months | Color the month markers with a per-month hue; on by default |
| Split by Month | Add a gap between months; off by default |
| Show Avatar | Render the user's 8×8 avatar in the leftmost columns; off by default |
| Refresh | Refresh interval in minutes |
Behavior notes: the select button forces an immediate refresh; failed requests back off exponentially starting at 30 s, capped at every; until the first fetch succeeds the panel shows ..., which becomes an orange ? once a fetch has failed.
fxaGdh4z8w5m.ax
# @name GitHub Heatmap
# @desc GitHub contribution heatmap via external worker
# @author Golevka2001
# @version 1.5
# @config server text "Server URL" help="Worker endpoint URL"
# @config user text "Username" help="GitHub username"
# @config client_id text "(optional) CF Access Client ID"
# @config client_secret text "(optional) CF Access Client Secret"
# @config rainbow bool "Rainbow Months" default=1
# @config split bool "Split by Month" default=0
# @config avatar bool "Show Avatar" default=0
# @config every number "Refresh" default=60 min=1 unit=min
class GitHubHeatmap
var url, usr, client_id, client_secret, rainbow, split, avatar, pixels, err
# Refresh state: ticks counts down to the next fetch, span is the configured
# interval in seconds, retry is the current backoff delay, busy means a
# request is still out.
var ticks, span, retry, busy
def init()
self.url = ""
self.usr = ""
var srv = store.get("server")
var u = store.get("user")
if srv != nil && srv != "" && u != nil && u != ""
self.url = srv
self.usr = str(u)
end
var cid = store.get("client_id")
self.client_id = cid == nil ? "" : str(cid)
var secret = store.get("client_secret")
self.client_secret = secret == nil ? "" : str(secret)
var rb = store.get("rainbow")
self.rainbow = rb == nil ? true : rb
var sp = store.get("split")
self.split = sp == nil ? false : sp
var av = store.get("avatar")
self.avatar = av == nil ? false : av
self.pixels = []
self.err = false
self.ticks = 0
self.span = 60
self.retry = 30
self.busy = false
end
def setup()
self.loop()
end
def on_button(btn)
if btn == "select"
self.now()
end
end
def on_body(body, status)
shared.set("f", 0)
if status != 200 || body == nil
self.failed()
return
end
# Extract all numbers from the JSON array [n,n,n,...]. re.matchall is
# cheaper than json.load on a 96 KB heap, and the full response is small
# enough that find/keep is not needed.
var m = re.matchall("[0-9]+", body)
if m == nil || size(m) == 0
self.failed()
return
end
var n = size(m)
var px = []
for i : 0 .. n - 1
px.push(int(m[i]))
end
if size(px) != 256
self.failed()
return
end
self.ok()
self.pixels = px
end
def loop()
if self.url != "" && self.due(store.get("every"))
shared.set("f", 1)
http.get(self.url, / b, st -> self.on_body(b, st),
{'headers': {'CF-Access-Client-Id': self.client_id,
'CF-Access-Client-Secret': self.client_secret,
'X-User': self.usr,
'X-Rainbow': self.rainbow ? "1" : "0",
'X-Split': self.split ? "1" : "0",
'X-Avatar': self.avatar ? "1" : "0"}})
end
end
# Fires at most once per interval, never while a request is out or another
# app's TLS handshake is running (shared "f" flag, official convention).
# Raises busy, so only call it when a request will actually be issued.
def due(every)
self.span = max(60, num(every, 1) * 60)
if self.ticks > 0
self.ticks = self.ticks - 1
return false
end
if self.busy
return false
end
for k : shared.keys()
var n = size(k)
if n > 2 && k[n - 2 .. n - 1] == ".f" && shared.get(k) == 1
var a = shared.age(k)
if a != nil && a < 20000
return false
end
end
end
self.ticks = self.span
self.busy = true
return true
end
# failure backoff: retry at 30s, doubling, capped at the interval; raises
# err so a fetch with no data yet can show "?" instead of "..."
def failed()
self.busy = false
self.ticks = self.retry
self.retry = min(self.retry * 2, self.span)
self.err = true
end
def ok()
self.busy = false
self.retry = 30
self.err = false
end
# Manual refresh: clears ticks, leaves busy alone so a press while a
# request is out cannot fire a second one.
def now()
self.ticks = 0
self.retry = 30
end
def draw()
clear()
if self.pixels == nil || size(self.pixels) == 0
var s = self.err ? "?" : "..."
var w = text_ink_width(s)
text(int((width() - w) / 2), 6, s, self.err ? 0xCC7722 : 0x666666)
return
end
# 32x8 panel, row-major: pixel index i → column i/8, row i%8
for i : 0 .. size(self.pixels) - 1
var c = self.pixels[i]
if c != 0
pixel(int(i / 8), i - int(i / 8) * 8, c)
end
end
end
end
return GitHubHeatmap()
Install it on your AWTRIX NG
Add this flow to your AWTRIX NG. It will start appearing on your display after installation.
Unable to connect? Download the flow and add it in the Scripts section of your AWTRIX. Advanced users can also use this command:
Discussion 0
Ask a question, suggest a change or share how you use it.
No comments yet. Start the conversation.
More flows for AWTRIX NG Scripts or Social
Something wrong with this flow? Report it.
Have an idea?
Sign in to ask questions and join the conversation.