Skip to content

feat(bounties): drive the Bounty Board from Tabularium - #35

Merged
debba merged 3 commits into
mainfrom
feat/bounty-board-tabularium
Oct 13, 2026
Merged

debba merged 3 commits into
mainfrom
feat/bounty-board-tabularium

Conversation

@debba

@debba debba commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Closes #34. Depends on TabularisDB/tabularium#29.

What changes

The bounty board is built from Tabularium plugin requests instead of a hand-edited JSON file.

Badge Source
Open, Most wanted, Scoped, Coming soon approved request with no claims. The label is bounty.boardStatus.
In progress (replaces claimed) approved request that has at least one claim
Shipped fulfilled request: its plugin is listed in the registry

Build time

  • scripts/fetch-app-data.mjs:
    • Maps GET /api/requests?status=approved,fulfilled onto src/data/pluginBounties.json.
    • Bounty fields an admin left empty fall back to the last committed copy, then to neutral defaults.
    • Keeps the committed copy when the registry is down or doesn't support request review yet (today's production). The current board keeps building until Publish refreshed wiki screenshots and video demos #29 is deployed.
  • src/data/pluginBounties.json: converted to the new shape, matching what import:bounties writes. claimed becomes scoped, shipped bounties are linked to their plugin, and claimUrl is removed.

In the browser

  • useLiveBounties:
    • Makes one credentialed call to the registry and refreshes votes, claims, claimers, votedByMe and claimedByMe.
    • If the call fails, the board keeps the baked-in values.
  • Upvotes: on the card and in the modal, applied optimistically.
  • Claim and release: from the modal. Claimers are listed with their avatars and repositories.
  • Signed-out visitors: their vote or claim is saved, they go through the Tabularium sign-in, and the action is replayed when they come back. The modal for that bounty reopens.
  • Start building: for your own claims, the modal creates the repository through POST /api/requests/:id/scaffold.
    • Fields: name, org, private, connection type and UI extension for drivers, minimum version for themes.
    • If GitHub hasn't granted the workflow scope yet, a "Grant access" button links to the re-authorization page.
    • The local command (npm create @tabularis/plugin@latest <slug>) is always shown as an alternative.
  • New "Most voted" sort.
  • The page copy and the request form explain that requests are reviewed first.
  • CLAUDE.md documents the data flow and the rebuild webhook.

Deploy order

  1. Merge and deploy tabularium#29, then run bun run import:bounties against production.
  2. In Tabularium, add a Static site rebuild webhook (request.approved|rejected|claimed|unclaimed|fulfilled) pointing at the Vercel deploy hook.
  3. Merge this PR. The next build switches the board to registry data.

Testing

  • pnpm build: passes and builds /plugins/bounties (656 static pages). The board kept the committed copy, because production doesn't support request review yet.
  • tsc: no new errors. The only errors are the existing ones under notes/.
  • End to end against a local Tabularium on the Publish refreshed wiki screenshots and video demos #29 branch: I imported the 31 bounties, then ran buildBounties() against it. It produced 31 bounties with request ids, and their states and metadata were preserved.
  • Not tested in a browser against a live registry: votes, claims and scaffolding. They need Publish refreshed wiki screenshots and video demos #29 deployed, with tabularis.dev in ALLOWED_ORIGINS.

The board is now built from approved and shipped requests on Tabularium
instead of a hand-edited list: fetch-app-data maps them onto
pluginBounties.json, keeping the committed copy when the registry is down
or predates request review. Claimed bounties show as in progress until
their plugin is listed.

In the browser the board refreshes votes and claims with one call to the
registry, and signed-in visitors can upvote, claim or release a bounty and
create a repository scaffolded by @tabularis/create-plugin from the bounty
modal. Signed-out actions go through the Tabularium sign-in and are
replayed on return.

Refs #34
@debba debba self-assigned this Oct 13, 2026
@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
website Ready Ready Preview Oct 13, 2026 8:54pm UTC

Request Review

@kilo-code-bot

kilo-code-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/plugins/requests.ts 165 fetchLiveRequests returns null on any HTTP error, discarding all previously fetched pages
scripts/fetch-app-data.mjs 269 buildBounties throws if any request lacks status, failing the entire build

SUGGESTION

File Line Issue
scripts/fetch-app-data.mjs 220 boardStatus from registry is not validated against allowed values
src/components/pages/plugins/bounties/BountyGrid/useLiveBounties.ts 31 Unsafe type assertion live.claimers as BountyClaimer[]
src/lib/plugins/requests.ts 160 Live fetch limit (10 pages) differs from build-time fetch (50 pages)
Files Reviewed (6 files)
  • src/lib/plugins/requests.ts - 2 issues
  • scripts/fetch-app-data.mjs - 2 issues
  • src/components/pages/plugins/bounties/BountyGrid/useLiveBounties.ts - 1 issue
  • src/lib/plugins/bounties.ts
  • src/components/pages/plugins/bounties/BountyGrid/BountyGrid.tsx
  • src/components/pages/plugins/bounties/BountyGrid/BountyModal/BountyModal.tsx
  • src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.tsx
  • src/app/plugins/bounties/page.tsx

Fix these issues in Kilo Cloud

Previous Review Summaries (2 snapshots, latest commit dfe1153)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit dfe1153)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 file)
  • src/lib/plugins/requests.ts - Both previous issues fixed

Previous review (commit 79b9fde)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/plugins/requests.ts 167 fetchLiveRequests discards all live data if any single request is malformed

SUGGESTION

File Line Issue
src/lib/plugins/requests.ts 251 localScaffoldCommand shows literal <version> placeholder in user-facing command
Files Reviewed (19 files)
  • CLAUDE.md - Documentation updated for bounty board data flow
  • scripts/fetch-app-data.mjs - Build-time bounty fetching from Tabularium API
  • src/app/plugins/bounties/page.tsx - Bounty board page
  • src/components/pages/plugins/Plugin.data.ts - Status weights for sorting
  • src/components/pages/plugins/bounties/BountyGrid/BountyGrid.tsx - Grid with filters/sorting
  • src/components/pages/plugins/bounties/BountyGrid/useLiveBounties.ts - Live data merging & actions
  • src/components/pages/plugins/bounties/BountyGrid/BountyModal/BountyModal.tsx - Detail modal with vote/claim/scaffold
  • src/components/pages/plugins/bounties/BountyGrid/BountyCard/BountyCard.tsx - Bounty card component
  • src/components/pages/plugins/bounties/BountyGrid/BountyFilters/BountyFilters.tsx - Filter/sort controls
  • src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.tsx - Repository scaffold form
  • src/components/pages/plugins/bounties/BountyGrid/BountyVote/BountyVote.tsx - Vote button
  • src/components/pages/plugins/bounties/BountyGrid/BountyClaimers/BountyClaimers.tsx - Claimers display
  • src/components/pages/plugins/bounties/BountyGrid/BountyMeta/BountyMeta.tsx - Meta display
  • src/components/pages/plugins/bounties/BountyGrid/BountyStatus/BountyStatus.tsx - Status badge
  • src/components/pages/plugins/bounties/RequestPluginButton/PluginRequestModal/PluginRequestModal.tsx - Plugin request form
  • src/lib/plugins/bounties.ts - Bounty types, status logic, stats
  • src/lib/plugins/requests.ts - API client for votes, claims, scaffolding
  • src/data/pluginBounties.json - Committed bounty data (31 entries)
  • src/components/pages/plugins/bounties/BountyGrid/BountyIcon/BountyIcon.tsx - (unchanged, context only)

Detailed Findings

WARNING: fetchLiveRequests discards all live data on any malformed request (src/lib/plugins/requests.ts:167)

if (body.requests.some((request) => !request.status || !Array.isArray(request.claimers))) return null;

If the registry returns 100 requests and just one is missing status or claimers, the entire live dataset is discarded and the UI falls back to stale build-time data. Consider filtering out malformed entries instead:

const validRequests = body.requests.filter((r) => r.status && Array.isArray(r.claimers));
if (validRequests.length !== body.requests.length) {
    console.warn(`${body.requests.length - validRequests.length} malformed requests dropped`);
}
for (const request of validRequests) bySlug.set(request.slug, request);

SUGGESTION: localScaffoldCommand shows literal <version> placeholder (src/lib/plugins/requests.ts:251)

return kind === 'theme'
    ? `npx --package @tabularis/create-plugin tabularis-theme scaffold ${slug} --dir ${slug} --min-runtime-version <version>`
    : `npm create @tabularis/plugin@latest ${slug}`;

The <version> placeholder renders literally in the UI. Consider using a clearer placeholder like --min-runtime-version 0.x.x or --min-runtime-version <VERSION> with angle brackets, or document that users must replace it.

Notes

  • The PR correctly implements the bounty board driven by Tabularium plugin requests with live votes/claims
  • Static export constraints are respected (no SSR, API routes, or middleware)
  • Optimistic UI updates with rollback on error are properly implemented
  • Sign-in round-trip via sessionStorage for pending actions is a solid pattern
  • TypeScript types are consistent across build-time data and live API responses
  • pnpm build passes per PR description; no new TypeScript errors introduced

Reviewed by free · Input: 401.9K · Output: 11.9K · Cached: 1.7M

Comment thread src/lib/plugins/requests.ts Outdated
if (!res.ok) return null;
const body = (await res.json()) as {total: number; requests: LiveRequest[]};
// A registry without request review has no status or claimers: keep the build data.
if (body.requests.some((request) => !request.status || !Array.isArray(request.claimers))) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: fetchLiveRequests discards all live data if any single request is malformed

If the registry returns 100 requests and just one is missing status or claimers, the entire live dataset is discarded and the UI falls back to stale build-time data. Consider filtering out malformed entries instead:

Suggested change
if (body.requests.some((request) => !request.status || !Array.isArray(request.claimers))) return null;
const validRequests = body.requests.filter((r) => r.status && Array.isArray(r.claimers));
if (validRequests.length !== body.requests.length) {
console.warn(`${body.requests.length - validRequests.length} malformed requests dropped`);
}
for (const request of validRequests) bySlug.set(request.slug, request);

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dfe1153: malformed entries are skipped (with a console warning) and the rest of the live data is kept. Against a registry that predates request review every entry is skipped, so the board still falls back to the build data.

Comment thread src/lib/plugins/requests.ts Outdated
/** Local alternative to the hosted scaffold, for any forge. */
export function localScaffoldCommand(slug: string, kind: string | null): string {
return kind === 'theme'
? `npx --package @tabularis/create-plugin tabularis-theme scaffold ${slug} --dir ${slug} --min-runtime-version <version>`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: localScaffoldCommand shows literal <version> placeholder in user-facing command

The <version> placeholder renders literally in the UI. Consider using a clearer placeholder like --min-runtime-version 0.x.x or --min-runtime-version <VERSION> with angle brackets, or document that users must replace it.

Suggested change
? `npx --package @tabularis/create-plugin tabularis-theme scaffold ${slug} --dir ${slug} --min-runtime-version <version>`
return kind === 'theme'
? `npx --package @tabularis/create-plugin tabularis-theme scaffold ${slug} --dir ${slug} --min-runtime-version 0.x.x`
: `npm create @tabularis/plugin@latest ${slug}`;

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dfe1153: the local theme command is prefilled with the current Tabularis version (APP_VERSION) instead of a placeholder.

Skip only the live requests without review data instead of discarding the
whole response, and prefill the local theme scaffold command with the
current Tabularis version instead of a <version> placeholder.

Refs #34
First snapshot generated from registry.tabularis.dev/api/requests, with
the request ids the board needs to vote and claim.

Refs #34
@debba
debba merged commit 21b689c into main Oct 13, 2026
3 checks passed
`${TABULARIUM_URL}/api/requests?status=approved,fulfilled&limit=100&page=${page}`,
{credentials: 'include'},
);
if (!res.ok) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: fetchLiveRequests returns null on any HTTP error, discarding all previously fetched pages

If a request for page 3 fails (e.g., transient network error, rate limit), the function returns null immediately, losing data from pages 1-2. The UI then falls back to stale build-time data. Consider returning partial data or at least logging the error:

Suggested change
if (!res.ok) return null;
if (!res.ok) {
console.warn(`fetchLiveRequests: page ${page} failed with ${res.status}`);
return bySlug.size > 0 ? bySlug : null;
}

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if (requests.length >= res.total || res.requests.length === 0) break;
}
// A registry that predates request review returns no status: nothing to build from.
if (requests.some((request) => !request.status)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: buildBounties throws if any request lacks status, failing the entire build

The check requests.some((request) => !request.status) throws an error if even a single request is missing a status. The comment says "A registry that predates request review returns no status" — implying ALL requests would lack status. But if the registry has mixed old/new data, this fails unnecessarily. Filter instead of throwing:

Suggested change
if (requests.some((request) => !request.status)) {
// A registry that predates request review returns no status: filter those out.
const validRequests = requests.filter((request) => request.status);
if (validRequests.length === 0) {
throw new Error('the registry does not expose request review status yet');
}
return {
constellationIds: committed.constellationIds,
bounties: validRequests.map((request) => toBounty(request, previous.get(request.slug))),
};

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

function toBounty(request, previous) {
const b = request.bounty ?? {};
const h = hashOf(request.slug);
const boardStatus = b.boardStatus ?? previous?.boardStatus ?? 'open';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: boardStatus from registry is not validated against allowed values

b.boardStatus is used directly without checking if it's a valid BOARD_STATUS value (most-wanted, open, scoped, coming-soon). An invalid value from the registry would propagate to the UI and could cause the status badge to render incorrectly or crash. Add validation with a fallback:

Suggested change
const boardStatus = b.boardStatus ?? previous?.boardStatus ?? 'open';
const allowedBoardStatus = ['most-wanted', 'open', 'scoped', 'coming-soon'];
const boardStatus = allowedBoardStatus.includes(b.boardStatus) ? b.boardStatus : (previous?.boardStatus ?? 'open');

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

...bounty,
requestId: live.id,
upvotes: live.upvotes,
claimers: live.claimers as BountyClaimer[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Unsafe type assertion live.claimers as BountyClaimer[]

The shapes currently match, but a type guard would be safer for future-proofing against registry API changes. Consider adding a runtime check or using a type predicate:

Suggested change
claimers: live.claimers as BountyClaimer[],
claimers: (live.claimers as BountyClaimer[]).map(c => ({ username: c.username ?? null, displayName: c.displayName, avatarUrl: c.avatarUrl ?? null, repoUrl: c.repoUrl ?? null })),

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

export async function fetchLiveRequests(): Promise<Map<string, LiveRequest> | null> {
try {
const bySlug = new Map<string, LiveRequest>();
for (let page = 1; page <= 10; page += 1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Live fetch limit (10 pages) differs from build-time fetch (50 pages)

fetchLiveRequests fetches up to 10 pages (1,000 requests) while buildBounties fetches up to 50 pages (5,000 requests). If the registry grows beyond 1,000 approved/fulfilled requests, live data will be incomplete. Align the limits for consistency.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

This branch was successfully deployed

1 active deployment
Preview — 546c09fb Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Drive the Plugin Bounty Board from Tabularium (live upvotes, claims, scaffolding)

1 participant