diff options
| author | Frank <[email protected]> | 2025-10-07 09:17:05 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-10-07 09:17:08 -0400 |
| commit | 6c99b833e443ba1531845a7fcf4f74247a0837bc (patch) | |
| tree | 8ad2e4c1bc3b67f44019d5dcbccc54b3fe2d0959 /packages/console/app/src | |
| parent | cd3780b7f5b46f03b121dff6172adb445bd748e5 (diff) | |
| download | opencode-6c99b833e443ba1531845a7fcf4f74247a0837bc.tar.gz opencode-6c99b833e443ba1531845a7fcf4f74247a0837bc.zip | |
wip: zen
Diffstat (limited to 'packages/console/app/src')
| -rw-r--r-- | packages/console/app/src/routes/workspace/member-section.tsx | 83 | ||||
| -rw-r--r-- | packages/console/app/src/routes/zen/handler.ts | 49 |
2 files changed, 116 insertions, 16 deletions
diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx index 9fc57621c..ddaac7348 100644 --- a/packages/console/app/src/routes/workspace/member-section.tsx +++ b/packages/console/app/src/routes/workspace/member-section.tsx @@ -56,25 +56,36 @@ const removeMember = action(async (form: FormData) => { ) }, "member.remove") -const updateMemberRole = action(async (form: FormData) => { +const updateMember = action(async (form: FormData) => { "use server" + console.log("!@#!@ Form data entries:") + for (const [key, value] of form.entries()) { + console.log(`!@#!@ ${key}:`, value) + } + const id = form.get("id")?.toString() if (!id) return { error: "ID is required" } const workspaceID = form.get("workspaceID")?.toString() if (!workspaceID) return { error: "Workspace ID is required" } const role = form.get("role")?.toString() as (typeof UserRole)[number] if (!role) return { error: "Role is required" } + const limit = form.get("limit")?.toString() + const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null + if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" } + + console.log({ id, role, monthlyLimit, limit }) + return json( await withActor( () => - User.updateRole({ id, role }) + User.update({ id, role, monthlyLimit }) .then((data) => ({ error: undefined, data })) .catch((e) => ({ error: e.message as string })), workspaceID, ), { revalidate: listMembers.key }, ) -}, "member.updateRole") +}, "member.update") export function MemberCreateForm() { const params = useParams() @@ -155,7 +166,7 @@ export function MemberCreateForm() { function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) { const [editing, setEditing] = createSignal(false) - const submission = useSubmission(updateMemberRole) + const submission = useSubmission(updateMember) const isCurrentUser = () => props.currentUserID === props.member.id createEffect(() => { @@ -164,6 +175,29 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str } }) + function getUsageDisplay() { + const currentUsage = (() => { + const dateLastUsed = props.member.timeMonthlyUsageUpdated + if (!dateLastUsed) return 0 + + const current = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", + }) + const lastUsed = dateLastUsed.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", + }) + if (current !== lastUsed) return 0 + return ((props.member.monthlyUsage ?? 0) / 100000000).toFixed(2) + })() + + const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit" + return `$${currentUsage} / ${limit}` + } + return ( <Show when={editing()} @@ -171,6 +205,7 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str <tr> <td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td> <td data-slot="member-role">{props.member.role}</td> + <td data-slot="member-usage">{getUsageDisplay()}</td> <Show when={!props.member.timeSeen} fallback={<td data-slot="member-joined"></td>}> <td data-slot="member-joined">invited</td> </Show> @@ -190,12 +225,21 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str } > <tr> - <td colspan="4"> - <form action={updateMemberRole} method="post"> + <td colspan="5"> + <form action={updateMember} method="post"> <div data-slot="edit-member-email">{props.member.accountEmail ?? props.member.email}</div> <input type="hidden" name="id" value={props.member.id} /> <input type="hidden" name="workspaceID" value={props.workspaceID} /> - <Show when={!isCurrentUser()} fallback={<div data-slot="current-user-role">Role: {props.member.role}</div>}> + + <Show + when={!isCurrentUser()} + fallback={ + <> + <div data-slot="current-user-role">Role: {props.member.role}</div> + <input type="hidden" name="role" value={props.member.role} /> + </> + } + > <div data-slot="role-selector"> <label> <input type="radio" name="role" value="admin" checked={props.member.role === "admin"} /> @@ -213,18 +257,32 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str </label> </div> </Show> + + <div data-slot="limit-selector"> + <label> + <strong>Monthly Limit</strong> + <input + type="number" + name="limit" + value={props.member.monthlyLimit ?? ""} + placeholder="No limit" + min="0" + /> + <p>Set a monthly spending limit for this user</p> + </label> + </div> + <Show when={submission.result && submission.result.error}> {(err) => <div data-slot="form-error">{err()}</div>} </Show> + <div data-slot="form-actions"> <button type="button" data-color="ghost" onClick={() => setEditing(false)}> Cancel </button> - <Show when={!isCurrentUser()}> - <button type="submit" data-color="primary" disabled={submission.pending}> - {submission.pending ? "Saving..." : "Save"} - </button> - </Show> + <button type="submit" data-color="primary" disabled={submission.pending}> + {submission.pending ? "Saving..." : "Save"} + </button> </div> </form> </td> @@ -258,6 +316,7 @@ export function MemberSection() { <tr> <th>Email</th> <th>Role</th> + <th>Usage</th> <th></th> <th></th> </tr> diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/handler.ts index 9d08ccdf9..feb0c9c3e 100644 --- a/packages/console/app/src/routes/zen/handler.ts +++ b/packages/console/app/src/routes/zen/handler.ts @@ -11,6 +11,7 @@ import { Billing } from "../../../../core/src/billing" import { Actor } from "@opencode-ai/console-core/actor.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { ZenModel } from "@opencode-ai/console-core/model.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" export async function handler( input: APIEvent, @@ -33,6 +34,7 @@ export async function handler( class AuthError extends Error {} class CreditsError extends Error {} class MonthlyLimitError extends Error {} + class UserLimitError extends Error {} class ModelError extends Error {} type Model = z.infer<typeof ZenModel.ModelSchema> @@ -181,6 +183,7 @@ export async function handler( error instanceof AuthError || error instanceof CreditsError || error instanceof MonthlyLimitError || + error instanceof UserLimitError || error instanceof ModelError ) return new Response( @@ -243,10 +246,15 @@ export async function handler( monthlyLimit: BillingTable.monthlyLimit, monthlyUsage: BillingTable.monthlyUsage, timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated, + userID: UserTable.id, + userMonthlyLimit: UserTable.monthlyLimit, + userMonthlyUsage: UserTable.monthlyUsage, + timeUserMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated, }) .from(KeyTable) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) + .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID))) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .then((rows) => rows[0]), ) @@ -269,6 +277,12 @@ export async function handler( monthlyUsage: data.monthlyUsage, timeMonthlyUsageUpdated: data.timeMonthlyUsageUpdated, }, + user: { + id: data.userID, + monthlyLimit: data.userMonthlyLimit, + monthlyUsage: data.userMonthlyUsage, + timeMonthlyUsageUpdated: data.timeUserMonthlyUsageUpdated, + }, isFree, } } @@ -280,19 +294,34 @@ export async function handler( const billing = authInfo.billing if (!billing.paymentMethodID) throw new CreditsError("No payment method") if (billing.balance <= 0) throw new CreditsError("Insufficient balance") + + const now = new Date() + const currentYear = now.getUTCFullYear() + const currentMonth = now.getUTCMonth() if ( billing.monthlyLimit && billing.monthlyUsage && billing.timeMonthlyUsageUpdated && billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) ) { - const now = new Date() - const currentYear = now.getUTCFullYear() - const currentMonth = now.getUTCMonth() const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear() const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth() if (currentYear === dateYear && currentMonth === dateMonth) - throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`) + throw new MonthlyLimitError( + `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}.`, + ) + } + + if ( + authInfo.user.monthlyLimit && + authInfo.user.monthlyUsage && + authInfo.user.timeMonthlyUsageUpdated && + authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100) + ) { + const dateYear = authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() + const dateMonth = authInfo.user.timeMonthlyUsageUpdated.getUTCMonth() + if (currentYear === dateYear && currentMonth === dateMonth) + throw new UserLimitError(`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}.`) } } @@ -386,6 +415,18 @@ export async function handler( timeMonthlyUsageUpdated: sql`now()`, }) .where(eq(BillingTable.workspaceID, authInfo.workspaceID)) + await tx + .update(UserTable) + .set({ + monthlyUsage: sql` + CASE + WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${cost} + ELSE ${cost} + END + `, + timeMonthlyUsageUpdated: sql`now()`, + }) + .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))) }) await Database.use((tx) => |
