From 089a94ad74c0ec5f57903c1c5ffa6b48fec7ac67 Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Tue, 21 Apr 2026 18:25:06 +0300 Subject: [PATCH 1/2] feat(wave): add per-repo budget override in admin panel --- src/lib/utils/wave/types/waveProgram.ts | 1 + src/lib/utils/wave/wavePrograms.ts | 30 +++++ .../wave/(base-layout)/admin/repos/+layout.ts | 4 +- .../components/budget-override-modal.svelte | 113 ++++++++++++++++++ .../admin/repos/repos/+page.svelte | 23 ++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte diff --git a/src/lib/utils/wave/types/waveProgram.ts b/src/lib/utils/wave/types/waveProgram.ts index 9d9b7089c..74575134d 100644 --- a/src/lib/utils/wave/types/waveProgram.ts +++ b/src/lib/utils/wave/types/waveProgram.ts @@ -137,6 +137,7 @@ export const waveProgramRepoWithDetailsDtoSchema = z.object({ pointsUsed: z.number().int(), pointsBudget: z.number().int().nullable(), pointsRemaining: z.number().int().nullable(), + pointsBudgetOverride: z.number().int().nullable().optional(), pointsMultiplier: z.number().int().optional(), repo: z.object({ stargazersCount: z.number().int().nullable().optional(), diff --git a/src/lib/utils/wave/wavePrograms.ts b/src/lib/utils/wave/wavePrograms.ts index 5b08ad86a..133a6f28c 100644 --- a/src/lib/utils/wave/wavePrograms.ts +++ b/src/lib/utils/wave/wavePrograms.ts @@ -184,6 +184,36 @@ export async function unfeatureWaveProgramRepo( }); } +export async function setRepoBudgetOverride( + f = fetch, + waveProgramId: string, + orgRepoId: string, + pointsBudget: number, +) { + await authenticatedCall( + f, + `/api/wave-programs/${waveProgramId}/repos/${orgRepoId}/budget-override`, + { + method: 'PUT', + body: JSON.stringify({ pointsBudget }), + }, + ); +} + +export async function removeRepoBudgetOverride( + f = fetch, + waveProgramId: string, + orgRepoId: string, +) { + await authenticatedCall( + f, + `/api/wave-programs/${waveProgramId}/repos/${orgRepoId}/budget-override`, + { + method: 'DELETE', + }, + ); +} + export async function addIssueToWaveProgram( f = fetch, waveProgramId: string, diff --git a/src/routes/(pages)/wave/(base-layout)/admin/repos/+layout.ts b/src/routes/(pages)/wave/(base-layout)/admin/repos/+layout.ts index f57398683..f557a4e0b 100644 --- a/src/routes/(pages)/wave/(base-layout)/admin/repos/+layout.ts +++ b/src/routes/(pages)/wave/(base-layout)/admin/repos/+layout.ts @@ -9,8 +9,9 @@ export const load = async ({ parent, fetch, depends }) => { const canManageTags = user.permissions?.includes('manageTags'); const canFeatureRepos = user.permissions?.includes('featureWaveRepos'); + const canManagePoints = user.permissions?.includes('managePoints'); - if (!canManageTags && !canFeatureRepos) { + if (!canManageTags && !canFeatureRepos && !canManagePoints) { throw redirect(302, '/wave/admin'); } @@ -24,5 +25,6 @@ export const load = async ({ parent, fetch, depends }) => { tags, canManageTags: !!canManageTags, canFeatureRepos: !!canFeatureRepos, + canManagePoints: !!canManagePoints, }; }; diff --git a/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte b/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte new file mode 100644 index 000000000..0c9b329b3 --- /dev/null +++ b/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte @@ -0,0 +1,113 @@ + + + + + diff --git a/src/routes/(pages)/wave/(base-layout)/admin/repos/repos/+page.svelte b/src/routes/(pages)/wave/(base-layout)/admin/repos/repos/+page.svelte index 1ecb67aca..daecc3abf 100644 --- a/src/routes/(pages)/wave/(base-layout)/admin/repos/repos/+page.svelte +++ b/src/routes/(pages)/wave/(base-layout)/admin/repos/repos/+page.svelte @@ -19,6 +19,8 @@ import type { Pagination } from '$lib/utils/wave/types/pagination'; import ManageRepoTagsModal from '../components/manage-repo-tags-modal.svelte'; import FeatureRepoModal from '../components/feature-repo-modal.svelte'; + import BudgetOverrideModal from '../components/budget-override-modal.svelte'; + import Coin from '$lib/components/icons/Coin.svelte'; let { data } = $props(); @@ -26,6 +28,7 @@ let wavePrograms = $derived(data.wavePrograms); let canManageTags = $derived(data.canManageTags); let canFeatureRepos = $derived(data.canFeatureRepos); + let canManagePoints = $derived(data.canManagePoints); let waveProgramId = $derived(data.waveProgramId); let repos = $derived(data.repos); @@ -123,6 +126,14 @@ }); } + function openBudgetOverrideModal(repo: WaveProgramRepoWithDetailsDto) { + modal.show(BudgetOverrideModal, undefined, { + repo, + waveProgramId: waveProgramId!, + onChanged: reloadPage, + }); + } + async function handleUnfeature(repo: WaveProgramRepoWithDetailsDto) { await doWithConfirmationModal( `Remove featured status from ${repo.repo.gitHubRepoFullName}?`, @@ -196,6 +207,18 @@ > {/if} {/if} + + {#if canManagePoints} + + {/if} {/snippet} From 91953695f72ad63b833d12453c353604e298223e Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Wed, 22 Apr 2026 13:02:35 +0300 Subject: [PATCH 2/2] fix(wave): align budget override checks and harden number parsing Align hasExistingOverride to use `!= null` consistently with the repo card button, and replace parseInt with Number() + isInteger validation to prevent silent truncation of decimal or exponent input. --- .../repos/components/budget-override-modal.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte b/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte index 0c9b329b3..780b7070d 100644 --- a/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte +++ b/src/routes/(pages)/wave/(base-layout)/admin/repos/components/budget-override-modal.svelte @@ -17,16 +17,16 @@ let { repo, waveProgramId, onChanged }: Props = $props(); - let hasExistingOverride = $derived( - repo.pointsBudgetOverride != null && repo.pointsBudgetOverride > 0, - ); + let hasExistingOverride = $derived(repo.pointsBudgetOverride != null); - let budget = $state(hasExistingOverride ? String(repo.pointsBudgetOverride) : ''); + let budget = $state(repo.pointsBudgetOverride != null ? String(repo.pointsBudgetOverride) : ''); let submitting = $state(false); let error = $state(null); - let budgetNum = $derived(parseInt(budget, 10)); - let canSubmit = $derived(!isNaN(budgetNum) && budgetNum >= 1 && !submitting); + let budgetNum = $derived(Number(budget)); + let canSubmit = $derived( + Number.isFinite(budgetNum) && Number.isInteger(budgetNum) && budgetNum >= 1 && !submitting, + ); async function handleSubmit() { if (!canSubmit) return;