- Python 49.3%
- JavaScript 23.4%
- HTML 11.9%
- CSS 11.2%
- Shell 4.2%
server.py was ~983 lines. Move the stateless logic — .env parsing, JQL + label/assignee/status-page filter helpers, and the raw-Jira -> slim-issue transform/sort — into jira_core.py. server.py (now ~765) keeps the runtime state, Jira HTTP client (coupled to CONFIG/AUTH_HEADER globals), request handlers, and main, and re-imports the pure names so the existing suite keeps referencing server.X. No signatures or behavior changed; all 68 tests pass and the live endpoints are unchanged. |
||
|---|---|---|
| docs/plans | ||
| public | ||
| scripts | ||
| systemd | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| config.example.json | ||
| jira_core.py | ||
| README.md | ||
| SAL-MAN-001 Profile Manual (1).pdf | ||
| server.py | ||
Rift Dynamics — Workshop Kanban Display
A wall display for the workshop. A Raspberry Pi 5 in kiosk mode shows the live KAN board: who's working on what, sorted by priority and due date, with three switchable layouts.
┌───────────────────────────────────────┐
│ ⌖ RIFT DYNAMICS KAN — Workshop │ Dark Gray
│ ● 2 min ago │ Eggshell
│ │ Vivid Orange = priority + active
│ ALICE BOB │
│ ▮ KAN-127 In Progress │
│ Mill housing v2 · Sprint 12 │
│ ▯ KAN-091 To Do │
│ Drawer sub-assembly │
│ … │
│ │
│ ⌖ MADE IN NO │
└───────────────────────────────────────┘
Overview
- Brand: Rift Dynamics — Dark Gray
#2C2C2C+ Eggshell#FAEFDD+ Vivid Orange#DB4C15. Space Mono Bold headings, Inter body. - Architecture: A small Python 3.11+ stdlib HTTP proxy on
127.0.0.1:8080holds the API token from.env, calls Atlassian Jira REST API v3, and serves a static HTML/JS frontend. The browser never sees the token. - Display: Portrait 1920×1080. Three switchable layouts (
columns/grid/swimlanes). Auto-refresh every 5 minutes;Rto force-refresh. Footer shows a live clock, date, and ISO week (HH:MM:SS | DD.MM.YYYY | Week NN). Below it, a news-style ticker scrolls urgent tasks (overdue in orange / due within 7 days / Highest+High priority) — continuous movement that also keeps the panel from idling on a static image. - Status pages: The workshop KAN board has 7 statuses, grouped into 3 pages — Backlog (Idea / Backlog / On hold), In Flight (Flighttesting / In work — default), Review (Review / Complete). Cycle pages with the arrow keys.
- Outage handling: Three distinct pill states —
auth_broken(bad token),upstream_error(Jira down),proxy_unreachable(proxy crashed). Cards stay on screen across all three.
Quick Start (local dev)
cp .env.example .env
# Edit .env and paste your Atlassian API token (https://id.atlassian.com/manage-profile/security/api-tokens)
python3 server.py
# → Listening on http://127.0.0.1:8080
Open http://127.0.0.1:8080/ in any browser.
| Key | Action |
|---|---|
1 / 2 / 3 |
Switch layout (columns / grid / swimlanes) |
→ / ↓ |
Next status page (Backlog → In Flight → Review → Backlog…) |
← / ↑ |
Previous status page |
R |
Force-refresh from Jira |
URL params persist your choice: /?layout=swimlanes&page=review lands on Review/Complete in swimlanes.
No token handy? Run the dev mock instead, which serves scripts/fixtures/issues.json:
python3 scripts/dev-mock-server.py
python3 -m pytest tests/ -q runs the server's unit suite (41 tests, ~4s).
Pi Installation
Assumes a freshly imaged Raspberry Pi OS (Bookworm or later) with the desktop environment. Verified on Raspberry Pi OS 13 "trixie" on a Raspberry Pi 5, which runs the labwc Wayland compositor — the Wayland-specific notes below (rotation, screen blanking, SSH key input) cover that stack.
# 1. clone onto the Pi. Private forge? First register the Pi's key as a repo Deploy Key:
# ssh-keygen -t ed25519 -N "" && cat ~/.ssh/id_ed25519.pub # paste into forge → repo → Deploy Keys
cd ~
git clone <repo-url> ws-display
cd ws-display
# 2. set up your token
cp .env.example .env
nano .env # paste a fresh API token from id.atlassian.com
# 3. install — sets up the user systemd service AND the kiosk autostart entry
bash scripts/install.sh
# 4. enable boot-time linger so the proxy runs without a logged-in session
sudo loginctl enable-linger "$USER"
# 5. reboot — kiosk launches automatically on desktop login
sudo reboot
Need Chromium? sudo apt install chromium wlopm (on trixie the package is chromium, not chromium-browser; wlopm drives the after-hours display off/on). unclutter is X11-only and unnecessary on a mouseless Wayland kiosk. trixie already ships Python 3.13 — no Python install needed.
Configuration
Credentials live in .env (see .env.example). The board to show and the default view can instead come from an optional config.json (see Switching the board below), which overrides .env without touching secrets.
| Key | Default | Purpose |
|---|---|---|
JIRA_URL |
— | Your Atlassian Cloud URL |
JIRA_EMAIL |
— | Atlassian account email |
JIRA_TOKEN |
— | API token (rotate periodically) |
JIRA_PROJECT_KEY |
KAN |
Project key to display |
JIRA_JQL |
see example | JQL filter for which issues to show |
MAX_CARDS_PER_ASSIGNEE |
6 |
Overflow becomes "+N more" |
POLL_INTERVAL_SECONDS |
300 |
How often the frontend re-fetches |
CACHE_TTL_SECONDS |
60 |
Server cache to soften bursts |
STALE_AFTER_MINUTES |
15 |
When "Updated X min ago" looks stale |
AVATAR_HOST_ALLOWLIST |
atlassian.net atl-paas.net |
SSRF guard for /api/avatar |
Switching the board (config.json)
To point the display at a different Jira project without editing credentials, drop a config.json next to server.py (gitignored, like .env; template in config.example.json):
{
"projectKey": "DDP2", // any project key; JQL auto-derived if "jql" is blank
"labels": ["DDP2", "urgent"], // optional: only issues with ANY of these labels
"assignees": [ // optional: only these people's tasks ("__UNASSIGNED__" = unassigned)
{ "accountId": "557058:abc", "displayName": "Alice" }
],
"jql": "", // optional full override (ignores projectKey/labels/assignees)
"defaultLayout": "columns", // columns | grid | swimlanes
"defaultPage": "active", // a page id: todo|active|done, or a custom id (p1/p2/p3)
"statusPages": [ // optional custom grouping; omit for auto To Do/In Progress/Done
{ "id": "p1", "label": "On the shelf", "statuses": ["Backlog", "On hold", "Idea"] },
{ "id": "p2", "label": "In Progress", "statuses": ["In work", "review"] },
{ "id": "p3", "label": "Done", "statuses": ["completed"] }
]
}
Then systemctl --user restart ws-display. By default pages are grouped by Jira's built-in status category (To Do / In Progress / Done) — any board works with no per-status setup. When a board's statuses are categorised oddly (e.g. KAN flags Backlog as In Progress), define statusPages to group by exact status name instead. labels filters the board to issues carrying any of the listed labels. The proxy exposes the current selection at GET /api/config.
Setup page (/setup)
Rather than editing config.json by hand, switch the board from a browser. Two .env keys turn it on:
BIND_HOST=0.0.0.0 # expose on the LAN (default 127.0.0.1 = localhost only)
SETUP_PASSWORD=some-pin # required; setup is DISABLED while blank
Restart, then open http://<pi-ip>:8080/setup (e.g. http://192.168.7.182:8080/setup), enter the password, and configure:
- Board + label filter (comma/space separated, any-of)
- Show tasks for — tick people to show only their tasks (reads the board's assignable users; includes an Unassigned option). None ticked = everyone.
- Default layout / page
- Custom page grouping (optional) — tick it to assign each of the board's statuses to a named page (e.g. put Backlog, On hold, Idea on an "On the shelf" page); leave it off for automatic To Do / In Progress / Done.
Save. The board updates within a few minutes; reboot the Pi to apply a new default view immediately.
⚠️ Security: the proxy holds your Jira token. Exposing it on the LAN makes the board read-only-viewable to anyone on the network; the setup/write endpoints (/setup, POST /api/config, /api/projects) require SETUP_PASSWORD. Plain HTTP means that password travels unencrypted — fine on a trusted workshop LAN, not on shared/guest wifi. To avoid LAN exposure entirely, leave BIND_HOST=127.0.0.1 and reach setup over an SSH tunnel: ssh -L 8080:localhost:8080 <user>@<pi> then open localhost:8080/setup.
Display Rotation (portrait)
The panel ships in landscape; the kiosk renders portrait. The method depends on the display stack.
Wayland / labwc (Raspberry Pi OS trixie — current default)
Rotation is owned by kanshi (started from /etc/xdg/labwc/autostart), so give it a profile:
mkdir -p ~/.config/kanshi
cat > ~/.config/kanshi/config <<'EOF'
profile workshop {
output HDMI-A-1 enable transform 270
}
EOF
transform is 90 / 180 / 270 / normal — pick 270 or 90 depending on which way the TV is mounted. Find your output name with wlr-randr, and test live (no reboot) with wlr-randr --output HDMI-A-1 --transform 270. kanshi re-applies the profile on every login.
Legacy X11 images (older Bookworm with the X session)
Firmware: add display_hdmi_rotate=1 (1 = left, 3 = right) to /boot/firmware/config.txt, reboot. Or runtime: add xrandr --output HDMI-1 --rotate left before the exec line in scripts/kiosk.sh (xrandr --query lists outputs). Note: display_hdmi_rotate is unreliable under the Pi 5 KMS driver — prefer the kanshi/Wayland route above.
Stop the screen from blanking
A wall display must never sleep. On Wayland/labwc:
sudo raspi-config # → Display Options → Screen Blanking → No
Reboot to apply.
Night mode (burn-in / power)
Two mechanisms — pick per panel:
A. Night shade (default — frontend, can't fail to wake). From 18:00 to 06:00 the kiosk covers the board with a black screen showing a dim clock + date that drifts to a new spot every minute — no static pixels, and panels with content-idle detection keep seeing movement. The HDMI signal never drops, so there's nothing to wake in the morning. Window: NIGHT_START_HOUR / NIGHT_END_HOUR at the top of public/night.js.
B. True power-off (systemd timers + wlopm — saves the most power). scripts/install.sh installs user timers that power the panel off at 18:00 / on at 06:00:
systemctl --user list-timers 'ws-display-*' # next off/on firing times
bash scripts/display-power.sh off # test now ('on' to wake)
⚠️ Verify your panel actually wakes when the signal returns. Older monitors (e.g. Samsung SyncMaster) enter a standby only the power button exits; on those, disable the off timer and rely on the shade:
systemctl --user disable --now ws-display-off.timer
Change times via OnCalendar= in ~/.config/systemd/user/ws-display-*.timer, then systemctl --user daemon-reload. Requires wlopm (sudo apt install wlopm); if missing at install time the timers are skipped with a notice.
Driving the display without a keyboard (over SSH)
Layout/page shortcuts are handled in the browser, so injecting them needs a Wayland input tool. Install wtype and point it at the running desktop session:
sudo apt install -y wtype
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
export WAYLAND_DISPLAY="$(basename "$(ls $XDG_RUNTIME_DIR/wayland-* | grep -v '\.lock$' | head -1)")"
wtype -k 1 # columns (2 = grid, 3 = swimlanes)
wtype -k Right # next status page (Left = previous)
wtype -k r # force refresh
For a permanent default view, instead set the URL in scripts/kiosk.sh — e.g. …/?layout=swimlanes&page=review.
Token Rotation
When the token expires or needs refresh:
- Generate a new token at id.atlassian.com → Security → API tokens.
- On the Pi:
nano ~/ws-display/.env # paste new value into JIRA_TOKEN= systemctl --user restart ws-display curl -fsS http://127.0.0.1:8080/api/health # expect ok:true within a few seconds - Revoke the old token in the same Atlassian panel.
If the new token is bad, the proxy enters degraded mode (no restart loop): the pill turns into "Auth failed — check .env" and you can fix and restart without rebooting.
Troubleshooting
| Symptom | Where to look |
|---|---|
| Pill is red "Auth failed — check .env" | journalctl --user -u ws-display -n 50 — proxy logs the auth failure on startup. Fix .env, systemctl --user restart ws-display. |
| Pill is red "Last sync Xm ago" | Network: ping rift-dynamics.atlassian.net. Workshop firewall blocking outbound HTTPS? |
| Pill is red "Proxy unreachable" | The proxy crashed or is restarting. systemctl --user status ws-display. After 5 crashes in 2 min systemd gives up — systemctl --user reset-failed ws-display && systemctl --user restart ws-display. |
| Blank black screen | Chromium didn't start. Run ~/ws-display/scripts/kiosk.sh from a desktop terminal; check chromium is installed (which chromium). |
| "Choose a password for new keyring" popup on screen | Chromium hitting the locked login keyring under auto-login. kiosk.sh passes --password-store=basic to prevent this — confirm that flag is still present. |
| Display stuck in landscape after reboot | kanshi profile missing or mistyped. Check ~/.config/kanshi/config and that the output name matches wlr-randr. |
| Fonts look wrong (system default sans) | ls public/fonts/ — both woff2 must be present. Re-clone if missing. |
| Cards don't update | Force a refresh: R on a plugged-in keyboard, wtype -k r over SSH (see above), or systemctl --user restart ws-display. Auto-refresh is every 5 min — adjust POLL_INTERVAL_SECONDS in .env. |
| Lots of "+N more" everywhere | Increase MAX_CARDS_PER_ASSIGNEE in .env and restart the proxy. |
For dev work without a real token, python3 scripts/dev-mock-server.py serves scripts/fixtures/issues.json.
License
Code: MIT. Brand assets (logos in public/assets/): © Rift Dynamics. Fonts (Inter, Space Mono): SIL OFL — see public/fonts/LICENSE.txt.