266266 <small >{{ task.issue_url ? `Issue #${task.issue_number}` : task.project_id }}</small >
267267 </td >
268268 <td >{{ task.required_worker_kind }}</td >
269- <td >{{ money (task.reward_cents) }}</td >
269+ <td >{{ mrg (task.reward_cents) }}</td >
270270 <td >{{ task.worker_id || task.suggested_agent_type || 'Unassigned' }}</td >
271271 <td ><span :class =" ['status-pill', task.status === 'accepted' ? 'blue' : 'amber']" >{{ task.status }}</span ></td >
272272 <td class =" task-pr-cell" >
288288 <div >
289289 <strong >#{{ pull.number }} {{ pull.title }}</strong >
290290 <small >@{{ pull.author }} / {{ pullStatus(pull) }} / {{ pull.head_ref || 'head' }} -> {{ pull.base_ref || 'base' }}</small >
291- <em >Credit: github:{{ pull.author }}</em >
291+ <em >Credit: github:{{ pull.author }} / {{ mrg(mergeSelection(task, pull).reward_mrg) }} </em >
292292 </div >
293293 </div >
294+ <div class =" bounty-review-controls" >
295+ <label >
296+ <span >Type</span >
297+ <select :value =" mergeSelection(task, pull).bounty_type" @change =" setMergeBounty(task, pull, $event.target.value)" >
298+ <option v-for =" option in bountyOptions" :key =" option.id" :value =" option.id" >{{ option.label }}</option >
299+ </select >
300+ </label >
301+ <label >
302+ <span >MRG</span >
303+ <input
304+ :value =" mergeSelection(task, pull).reward_mrg"
305+ min =" 1"
306+ step =" 1"
307+ type =" number"
308+ @input =" setMergeReward(task, pull, $event.target.value)"
309+ />
310+ </label >
311+ </div >
294312 <div class =" task-pr-actions" >
295313 <a class =" compact-action link-action" :href =" pull.html_url" target =" _blank" rel =" noreferrer" >View</a >
296314 <button class =" compact-action merge-action" :disabled =" !canMergeTaskPull(task, pull)" type =" button" @click =" mergeTaskPull(task, pull)" >
@@ -523,6 +541,7 @@ const taskPullsLoading = ref({});
523541const taskPullsError = ref ({});
524542const mergeBusy = ref ({});
525543const mergeMessages = ref ({});
544+ const mergeSelections = ref ({});
526545
527546const loginForm = reactive ({
528547 email: ' admin@gmail.com' ,
@@ -563,6 +582,13 @@ const viewByRoute = Object.entries(routeByView).reduce((routes, [view, route]) =
563582 return routes;
564583}, {});
565584
585+ const bountyOptions = [
586+ { id: ' future-small' , label: ' Future small' , reward_mrg: 25 },
587+ { id: ' future-medium' , label: ' Future medium' , reward_mrg: 50 },
588+ { id: ' bug-large' , label: ' Bug bounty large' , reward_mrg: 100 },
589+ { id: ' major-feature' , label: ' Major feature' , reward_mrg: 200 },
590+ ];
591+
566592const builderWidgets = [
567593 { id: ' metrics' , label: ' Metric Counter' , icon: BarChart3 },
568594 { id: ' project-list' , label: ' Project Queue' , icon: FolderKanban },
@@ -584,6 +610,7 @@ const query = computed(() => search.value.toLowerCase());
584610const selectedUser = computed (() => users .value .find ((row ) => row .id === selectedUserId .value ) || null );
585611const sslOkCount = computed (() => sslRows .value .filter ((row ) => row .status === ' ok' ).length );
586612const sslAttentionCount = computed (() => sslRows .value .length - sslOkCount .value );
613+ const tokenSymbol = computed (() => summary .value .token_symbol || ' MRG' );
587614
588615const summaryMetrics = computed (() => [
589616 { label: ' Users' , value: number (summary .value .user_count ), icon: UsersRound, tone: ' blue' },
@@ -874,15 +901,69 @@ function mergeKey(task, pull) {
874901 return ` ${task .id }: ${pull .number }` ;
875902}
876903
904+ function defaultBountyForTask(task = {}) {
905+ const reward = Number(task.reward_cents) || 25;
906+ return bountyOptions.find((option) => option.reward_mrg === reward) || bountyOptions[0];
907+ }
908+
909+ function ensureMergeSelection(task, pull) {
910+ if (!task?.id || !pull?.number) return;
911+ const key = mergeKey(task, pull);
912+ if (mergeSelections.value[key]) return;
913+ const option = defaultBountyForTask(task);
914+ mergeSelections.value = {
915+ ...mergeSelections.value,
916+ [key]: {
917+ bounty_type: option.id,
918+ reward_mrg: option.reward_mrg,
919+ },
920+ };
921+ }
922+
923+ function mergeSelection(task, pull) {
924+ ensureMergeSelection(task, pull);
925+ return mergeSelections.value[mergeKey(task, pull)] || {
926+ bounty_type: bountyOptions[0].id,
927+ reward_mrg: bountyOptions[0].reward_mrg,
928+ };
929+ }
930+
931+ function setMergeBounty(task, pull, value) {
932+ const option = bountyOptions.find((row) => row.id === value) || bountyOptions[0];
933+ const key = mergeKey(task, pull);
934+ mergeSelections.value = {
935+ ...mergeSelections.value,
936+ [key]: {
937+ bounty_type: option.id,
938+ reward_mrg: option.reward_mrg,
939+ },
940+ };
941+ }
942+
943+ function setMergeReward(task, pull, value) {
944+ const key = mergeKey(task, pull);
945+ const current = mergeSelection(task, pull);
946+ const reward = Math.max(1, Math.round(Number(value) || 0));
947+ mergeSelections.value = {
948+ ...mergeSelections.value,
949+ [key]: {
950+ ...current,
951+ reward_mrg: reward,
952+ },
953+ };
954+ }
955+
877956function pullStatus(pull) {
878957 if (pull.merged) return 'merged';
879958 if (pull.draft) return 'draft';
880959 return pull.state || 'open';
881960}
882961
883962function canMergeTaskPull(task, pull) {
963+ const selection = mergeSelection(task, pull);
884964 if (!pull?.author || task.status === 'accepted') return false;
885965 if (mergeBusy.value[mergeKey(task, pull)] || pull.draft) return false;
966+ if (!selection.bounty_type || Number(selection.reward_mrg) <= 0) return false;
886967 return pull.merged || pull.state === 'open';
887968}
888969
@@ -897,6 +978,9 @@ async function loadTaskPulls(task, force = false) {
897978 ...taskPulls.value,
898979 [task.id]: Array.isArray(payload.pull_requests) ? payload.pull_requests : [],
899980 };
981+ for (const pull of taskPulls.value[task.id]) {
982+ ensureMergeSelection(task, pull);
983+ }
900984 taskPullsLoaded.value = { ...taskPullsLoaded.value, [task.id]: true };
901985 } catch (error) {
902986 taskPullsError.value = { ...taskPullsError.value, [task.id]: error.message };
@@ -924,13 +1008,17 @@ async function loadPullsForVisibleTasks() {
9241008async function mergeTaskPull(task, pull) {
9251009 if (!canMergeTaskPull(task, pull)) return;
9261010 const key = mergeKey(task, pull);
1011+ const selection = mergeSelection(task, pull);
9271012 mergeBusy.value = { ...mergeBusy.value, [key]: true };
9281013 mergeMessages.value = { ...mergeMessages.value, [task.id]: '' };
9291014 taskPullsError.value = { ...taskPullsError.value, [task.id]: '' };
9301015 try {
9311016 const result = await api(` / api/ admin/ tasks/ ${encodeURIComponent (task .id )}/ pulls/ ${pull .number }/ merge` , {
9321017 method: 'POST',
933- body: JSON.stringify({}),
1018+ body: JSON.stringify({
1019+ bounty_type: selection.bounty_type,
1020+ reward_mrg: Number(selection.reward_mrg) || 0,
1021+ }),
9341022 });
9351023 if (result.task) {
9361024 tasks.value = tasks.value.map((row) => (row.id === result.task.id ? result.task : row));
@@ -941,7 +1029,8 @@ async function mergeTaskPull(task, pull) {
9411029 [task.id]: pullsForTask(task).map((row) => (row.number === result.pull_request.number ? result.pull_request : row)),
9421030 };
9431031 }
944- mergeMessages.value = { ...mergeMessages.value, [task.id]: ` Paid ${result .worker_id || ` github:${ pull .author } ` }.` };
1032+ const commentStatus = result.comment_error ? ` Comment failed: ${result .comment_error }` : ' Commented on PR.';
1033+ mergeMessages.value = { ...mergeMessages.value, [task.id]: ` Paid ${mrg (result .reward_mrg || selection .reward_mrg )} to ${result .worker_id || ` github:${ pull .author } ` }.$ {commentStatus}` };
9451034 const [summaryData, ledgerData] = await Promise.all([
9461035 api('/api/admin/summary'),
9471036 api('/api/admin/ledger'),
@@ -985,6 +1074,10 @@ function money(cents = 0) {
9851074 }).format((Number(cents) || 0) / 100);
9861075}
9871076
1077+ function mrg(value = 0) {
1078+ return ` ${number (value)} ${tokenSymbol .value }` ;
1079+ }
1080+
9881081function number(value = 0) {
9891082 return new Intl.NumberFormat('en-US').format(Number(value) || 0);
9901083}
0 commit comments