Extends the on-chain work-board into a full Dework-style community surface, white-labeled per domain, on our own primitives. Every view derives from ONE on-chain read (loadWorkspace) so header/board/leaderboard numbers can't drift. Views (all reading live Bounty/Reputation over JSON-RPC): - Overview: org header + Time-to-payment / Total-paid stats (from PaymentReleased + BountyProposed event timestamps), Discord role-gate banner, Bounties category grid (Spaces), Projects, right rail (About/Contributors/Admins). - Board: 4-lane kanban (To Do/In Progress/In Review/Done) with Dework empty-state framing; task cards show title + skill tags + reward + open-to badge + assignee. - Open Tasks: rows grouped by status with skill tags + reward + assignee. - Leaderboards: Top Contributors + Top Reviewers (7d/30d/all-time; windows from the event stream, all-time from Reputation) + All Contributors; global-Karma column. - Community Suggestions: create modal + upvotes (local-first, IAM/on-chain-swappable). - Combined Board (all Spaces) + Space tabs (Overview/Board/Open Tasks/Suggestions). - Task detail: on-chain description + real activity log (created→funded→claimed→ submitted→accepted→paid) + apply/comment flow + status/assignee/reviewers. - Explore: cross-space skill-filtered index + sibling-DAO links. Data layer: - chain.loadWorkspace assembles bounties + activity + stats + contributors (workers ∪ reviewers) in one pass; block timestamps batched. - Reward model is a native/ERC-20/ERC-721/ERC-1155 union — parseReward reads rewardType+tokenId when the finished Bounty struct carries them (NFT rewards render with tokenURI metadata), defaults to native/ERC-20 today. - Karma optional per brand (global soul-bound rep); degrades cleanly when absent. Config-driven white-label: Spaces/skills/tagline/accent/socials/Karma/IAM added to the existing VITE_BRAND brand map (zoo, pars); one canonical config, one image/brand. Hash router (no dep); Dework dark palette; per-brand accent via --brand. package: 0.1.0 -> 0.2.0 Co-authored-by: Hanzo Dev <[email protected]>
Work Board
White-label on-chain bounty kanban — a DAO's "dework". It reads a live BountyV1 / EscrowV1 / ReputationV1 work-market directly over JSON-RPC (no subgraph) and renders it as a kanban board. The differentiator is the portable on-chain reputation surfaced on every card: it lives in ReputationV1 and travels with the worker's address, not the platform.
One repo, one Dockerfile. A build-time brand (VITE_BRAND, default zoo)
selects the white-label profile — chain, addresses, owner label, header/title,
and the RPC host baked into the CSP — from src/brands.ts:
| Brand | Chain | Site |
|---|---|---|
zoo |
200200 | https://work.zoo.network |
pars |
494949 | https://work.pars.network |
Run
npm install
npm run dev # http://localhost:5173 (proxies /rpc -> 127.0.0.1:9631 port-forward)
npm run build # tsc --noEmit && vite build -> dist/
npm run preview # serve the production build
Point the dev proxy at any node instead of the local port-forward, and select a brand:
RPC_TARGET=https://api.pars.network npm run dev
VITE_BRAND=pars VITE_RPC_URL=https://api.pars.network/v1/bc/C/rpc npm run build
If the RPC is unreachable, the board renders a faithful fixture of that brand's live bounty #0 (behind the same render path) so the UI stays verifiable; the source badge shows FIXTURE.
Config
Brand-varying values live in one map: src/brands.ts; src/config.ts selects
the active brand from VITE_BRAND. To add a board, add a brand profile — it's a
data change, not a code change.
Deploy
CI (.github/workflows/docker.yml) builds on every push to main on luxfi's
in-cluster ARC pool (lux-build-amd64) and pushes to ghcr.io/luxfi/work-board
— never built locally. A brand matrix stamps one immutable tag per brand from
the same Dockerfile:
zoo→:sha-<commit>→ zoo-k8s, namespacezoo-mainnet(manifests ink8s/)pars→:pars-<commit>→ pars-k8s, namespacepars(manifests ink8s/pars/)
Each deployment.yaml + service.yaml runs 2 replicas serving dist on :3000,
Service :80 → :3000; pin the Deployment to the CI tag then kubectl apply. Each
route.yaml is the Traefik dynamic-config router (host → Service, TLS via the
ingress's built-in ACME resolver); hanzoai/ingress routes every public host
through a @file ConfigMap (zoo: zoo-system/zoo-dynamic; pars:
hanzo/pars-dynamic), not Ingress objects — merge the fragment in (see the
header in each route.yaml).
Data source
Zoo 200200
| Contract | Address |
|---|---|
| BountyV1 | 0x3EDb4a0104614b4aC12D5babCE984291aE8BE8E7 |
| EscrowV1 | 0x095E68282aea751Cc70A2Be565270f1B6AB0229C |
| ReputationV1 | 0xed976852e8c2b1283e4F475845046B679224460D |
| owner | 0x229599f227231d8C90fcF1a78589F5DC4b7A6962 (Zoo DAO Safe) |
Pars 494949
| Contract | Address |
|---|---|
| BountyV1 | 0x316B41c886c7D4B4e38cBB08a243776Ed977cf1F |
| EscrowV1 | 0xD5890D32d603a04E35ec7dBAbDCD0CA400f07E92 |
| ReputationV1 | 0x155d1363c23467929FB709FCFa0afC51F3497aB6 |
| owner | 0x4CEA4ac1C874a340B06e0422E77a477463C3a542 (Pars DAO Safe) |
bountyCount() + bounties(i) give the current struct; BountyProposed /
WorkSubmitted logs give issueRef / deliverableRef; ReputationV1.completedOf /
earnedOf give the worker's score. ABIs are copied into src/abi.ts (no cross-repo import).