No description
  • Python 49.3%
  • JavaScript 23.4%
  • HTML 11.9%
  • CSS 11.2%
  • Shell 4.2%
Find a file
Rift Dynamics Workshop 445253f3c1 refactor: extract pure helpers into jira_core.py (no behavior change)
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.
2026-06-09 08:27:56 +02:00
docs/plans chore(spec): mark plan VERIFIED — 7 tasks, 2 verify iterations, 41 tests passing 2026-05-27 09:19:18 +02:00
public feat: assignee (user) filter in setup 2026-06-08 16:22:05 +02:00
scripts feat: config-driven board selection + statusCategory grouping 2026-06-08 10:16:20 +02:00
systemd feat: scheduled display off 18:00 / on 06:00 (burn-in / power) 2026-06-03 16:18:42 +02:00
tests feat: assignee (user) filter in setup 2026-06-08 16:22:05 +02:00
.env.example feat: network setup page (/setup) with password gate + LAN bind 2026-06-08 10:28:48 +02:00
.gitignore feat: config-driven board selection + statusCategory grouping 2026-06-08 10:16:20 +02:00
config.example.json feat: assignee (user) filter in setup 2026-06-08 16:22:05 +02:00
jira_core.py refactor: extract pure helpers into jira_core.py (no behavior change) 2026-06-09 08:27:56 +02:00
README.md feat: assignee (user) filter in setup 2026-06-08 16:22:05 +02:00
SAL-MAN-001 Profile Manual (1).pdf chore: bootstrap project scaffolding 2026-05-26 16:37:38 +02:00
server.py refactor: extract pure helpers into jira_core.py (no behavior change) 2026-06-09 08:27:56 +02:00

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:8080 holds 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; R to 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:

  1. Generate a new token at id.atlassian.com → Security → API tokens.
  2. 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
    
  3. 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.