Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
ed531d9
chore(coderd/notifications): sync formatting and rendered-template go…
jaaydenh Aug 10, 2026
233795f
fix: enforce uniqueness and hour alignment for agent runtime usage ev…
jaaydenh Aug 10, 2026
3359ec5
feat: report stable license measurement diagnostics and tolerate unus…
jaaydenh Aug 10, 2026
69e8b95
feat: report agent runtime hours usage in entitlements
jaaydenh Aug 10, 2026
b485c5a
revert: "chore(coderd/notifications): sync formatting and rendered-te…
jaaydenh Aug 10, 2026
b7cae3d
Merge branch 'runtime-invariants' into license-measure-diagnostics
jaaydenh Aug 10, 2026
1d0dac8
Merge branch 'license-measure-diagnostics' into runtime-hours-entitle…
jaaydenh Aug 10, 2026
978b8d3
Merge remote-tracking branch 'origin/main' into runtime-invariants
jaaydenh Aug 10, 2026
258a368
Merge branch 'runtime-invariants' into license-measure-diagnostics
jaaydenh Aug 10, 2026
fa2f98b
Merge branch 'license-measure-diagnostics' into runtime-hours-entitle…
jaaydenh Aug 10, 2026
109ff26
updates for PR review
jaaydenh Aug 10, 2026
e679d63
updates for PR review
jaaydenh Aug 10, 2026
8f661c1
feat(enterprise/coderd/license): decode -1 runtime hours allocation a…
jaaydenh Aug 11, 2026
02b55b0
Merge branch 'license-measure-diagnostics' into runtime-hours-entitle…
jaaydenh Aug 11, 2026
c865ff2
feat: support unlimited agent runtime hours in entitlements
jaaydenh Aug 11, 2026
00d3249
chore: remove data-variant
jaaydenh Aug 11, 2026
3d8940c
chore(coderd/database): restore original InsertUsageEvent comment
jaaydenh Aug 11, 2026
0f8b1ab
Merge branch 'license-measure-diagnostics' into runtime-hours-entitle…
jaaydenh Aug 11, 2026
790c22d
Merge branch 'runtime-invariants' into runtime-hours-entitlements
jaaydenh Aug 11, 2026
8013af1
chore: remove and simplify explanatory comments
jaaydenh Aug 11, 2026
ef34856
Merge remote-tracking branch 'origin/main' into runtime-invariants
jaaydenh Aug 11, 2026
68b0295
fix(coderd/database): renumber agent runtime invariants migration to …
jaaydenh Aug 11, 2026
de3da4e
Merge branch 'runtime-invariants' into HEAD
jaaydenh Aug 11, 2026
1cbb069
Merge branch 'license-measure-diagnostics' into runtime-hours-entitle…
jaaydenh Aug 11, 2026
8d5606b
chore(enterprise/coderd/usage): restore agent runtime requeue guidanc…
jaaydenh Aug 11, 2026
27584cd
chore(site/src/modules/dashboard/LicenseBanner): remove comments narr…
jaaydenh Aug 11, 2026
f7447af
fix: make unlimited usage period features outrank metered ones on ties
jaaydenh Aug 11, 2026
f00afbb
feat(site/src/pages/DeploymentSettingsPage/LicensesSettingsPage): add…
jaaydenh Aug 12, 2026
2b97805
Merge remote-tracking branch 'origin/license-measure-diagnostics' int…
jaaydenh Aug 12, 2026
78e4477
fix(enterprise/coderd/license): accept issuer-valid zero soft limit f…
jaaydenh Aug 12, 2026
4661ddd
fix: address restack test failures and banner equality semantics
jaaydenh Aug 12, 2026
9d38c7a
Merge remote-tracking branch 'origin/main' into ri-fix
jaaydenh Aug 12, 2026
c8161e1
fix(coderd/database): renumber agent runtime invariants migration to …
jaaydenh Aug 12, 2026
f3374c4
Merge branch 'ri-fix' into license-measure-diagnostics
jaaydenh Aug 12, 2026
dbf6970
Merge remote-tracking branch 'origin/license-measure-diagnostics' int…
jaaydenh Aug 12, 2026
96fc0bc
chore(coderd/notifications): sync formatting and rendered-template go…
jaaydenh Aug 10, 2026
5c010c6
fix: enforce uniqueness and hour alignment for agent runtime usage ev…
jaaydenh Aug 10, 2026
cb36cfa
revert: "chore(coderd/notifications): sync formatting and rendered-te…
jaaydenh Aug 10, 2026
e127980
updates for PR review
jaaydenh Aug 10, 2026
d413cd9
chore(coderd/database): restore original InsertUsageEvent comment
jaaydenh Aug 11, 2026
273f068
fix(coderd/database): renumber agent runtime invariants migration to …
jaaydenh Aug 11, 2026
5e2f179
chore(enterprise/coderd/usage): restore agent runtime requeue guidanc…
jaaydenh Aug 11, 2026
c3ee285
fix(coderd/database): renumber agent runtime invariants migration to …
jaaydenh Aug 12, 2026
5779fcf
chore(enterprise/coderd/usage): correct ON CONFLICT concurrency comments
jaaydenh Aug 12, 2026
29bcb1c
feat: report stable license measurement diagnostics and tolerate unus…
jaaydenh Aug 10, 2026
5c5a928
feat(enterprise/coderd/license): decode -1 runtime hours allocation a…
jaaydenh Aug 11, 2026
1ec6150
chore: remove data-variant
jaaydenh Aug 11, 2026
2ed2fd5
chore: remove and simplify explanatory comments
jaaydenh Aug 11, 2026
3ea009a
fix: make unlimited usage period features outrank metered ones on ties
jaaydenh Aug 11, 2026
4b22ef0
fix(enterprise/coderd/license): accept issuer-valid zero soft limit f…
jaaydenh Aug 12, 2026
bfa6935
Merge remote-tracking branch 'origin/license-measure-diagnostics' int…
jaaydenh Aug 13, 2026
8fcb4b3
fix(coderd/notifications/dispatch): drop unused markdown import in sm…
jaaydenh Aug 13, 2026
a4bc61e
feat: report stable license measurement diagnostics and tolerate unus…
jaaydenh Aug 10, 2026
9d358f4
feat(enterprise/coderd/license): decode -1 runtime hours allocation a…
jaaydenh Aug 11, 2026
cd37639
chore: remove data-variant
jaaydenh Aug 11, 2026
99167d3
chore: remove and simplify explanatory comments
jaaydenh Aug 11, 2026
0fda91c
fix: make unlimited usage period features outrank metered ones on ties
jaaydenh Aug 11, 2026
97eab52
fix(enterprise/coderd/license): accept issuer-valid zero soft limit f…
jaaydenh Aug 12, 2026
829f829
Merge remote-tracking branch 'origin/license-measure-diagnostics' int…
jaaydenh Aug 13, 2026
a04611a
feat(enterprise/coderd/license): grandfather claim-less premium licen…
jaaydenh Aug 13, 2026
fa3091e
Merge remote-tracking branch 'origin/runtime-hours-entitlements' into…
jaaydenh Aug 13, 2026
e617d4e
chore(site/src/pages/DeploymentSettingsPage/LicensesSettingsPage): pi…
jaaydenh Aug 13, 2026
7e5b4b8
feat: report precise agent runtime milliseconds in entitlements
jaaydenh Aug 13, 2026
a0a5d33
Merge remote-tracking branch 'origin/runtime-hours-entitlements' into…
jaaydenh Aug 13, 2026
9c8ac85
feat(site/src/pages/DeploymentSettingsPage/LicensesSettingsPage): sho…
jaaydenh Aug 13, 2026
ea1ef4f
fix(site/src): identify winning agent hours license by merged usage p…
jaaydenh Aug 13, 2026
9a1e2c4
fix(site/src): match full usage period and render agents card on runt…
jaaydenh Aug 13, 2026
e73edd8
fix: fix storybook tests
jaaydenh Aug 17, 2026
4f0c694
chore: license page improvements
jaaydenh Aug 17, 2026
d4d1cc4
chore: design updates
jaaydenh Aug 18, 2026
5ad5e7d
fix: format
jaaydenh Aug 18, 2026
3fda674
chore(site/src): tighten comments in license products section
jaaydenh Aug 18, 2026
d1257d1
fix(site/src/pages/DeploymentSettingsPage/LicensesSettingsPage): repa…
jaaydenh Aug 18, 2026
bab65d8
Merge remote-tracking branch 'origin/main' into license-page-t2yd
jaaydenh Aug 18, 2026
1863dc2
fix(site/src/pages/DeploymentSettingsPage/LicensesSettingsPage): asse…
jaaydenh Aug 18, 2026
c26d4c0
fix(site/src/pages/DeploymentSettingsPage/LicensesSettingsPage): trea…
jaaydenh Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ type Claims = {
license_expires: number;
// nbf is a standard JWT claim for "not before" - the license valid from date
nbf?: number;
// iat is a standard JWT claim for "issued at"; the merged
// usage_period.issued_at is stamped from the winning license's iat.
iat?: number;
// exp is a standard JWT claim for "expires at" (end of grace period);
// it stamps usage_period.end, and nbf stamps usage_period.start.
exp?: number;
account_type?: string;
account_id?: string;
trial: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dayjs from "dayjs";
import type { GetLicensesResponse } from "#/api/api";
import type { Feature } from "#/api/typesGenerated";
import { isLicenseApplicableForFeatureUsage } from "./licenseApplicability";

function isPremiumLicense(license: GetLicensesResponse): boolean {
return license.claims.feature_set?.toLowerCase() === "premium";
Expand All @@ -19,24 +19,6 @@ export function licenseShowsAiGovernanceAddOn(
);
}

export function isLicenseApplicableForAiGovernanceOverage(
license: GetLicensesResponse,
aiGovernanceUserFeature: Feature | undefined,
): boolean {
const isExpired = dayjs
.unix(license.claims.license_expires)
.isBefore(dayjs());
const isNotYetValid =
license.claims.nbf !== undefined &&
dayjs.unix(license.claims.nbf).isAfter(dayjs());
const isAiGovernanceEntitlementInGracePeriod =
aiGovernanceUserFeature?.entitlement === "grace_period";

return (
!isNotYetValid && (!isExpired || isAiGovernanceEntitlementInGracePeriod)
);
}

export function hasAiGovernanceAddOnLicense(
licenses: GetLicensesResponse[] | undefined,
aiGovernanceUserFeature: Feature | undefined,
Expand All @@ -45,10 +27,7 @@ export function hasAiGovernanceAddOnLicense(
licenses?.some(
(license) =>
licenseShowsAiGovernanceAddOn(license) &&
isLicenseApplicableForAiGovernanceOverage(
license,
aiGovernanceUserFeature,
),
isLicenseApplicableForFeatureUsage(license, aiGovernanceUserFeature),
) ?? false
);
}
Expand All @@ -65,10 +44,7 @@ function aiGovernanceLimitFromLicenses(
.filter(
(license) =>
licenseShowsAiGovernanceAddOn(license) &&
isLicenseApplicableForAiGovernanceOverage(
license,
aiGovernanceUserFeature,
),
isLicenseApplicableForFeatureUsage(license, aiGovernanceUserFeature),
)
.map((license) => license.claims.features?.ai_governance_user_limit)
.filter((limit): limit is number => limit !== undefined);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
import { CoderAgentsProductCard } from "./CoderAgentsProductCard";

const meta: Meta<typeof CoderAgentsProductCard> = {
title:
"pages/DeploymentSettingsPage/LicensesSettingsPage/CoderAgentsProductCard",
component: CoderAgentsProductCard,
args: {
allocation: 20000,
actual: 16264.3,
isSoftLimitReached: false,
isExceeded: false,
isHardLimitExceeded: false,
},
};

export default meta;
type Story = StoryObj<typeof CoderAgentsProductCard>;

const getMetricValue = (canvas: ReturnType<typeof within>, label: string) =>
canvas.getByText(label).parentElement?.nextElementSibling;

export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Coder Agents")).toBeInTheDocument();
await expect(getMetricValue(canvas, "Total Agent hours")).toHaveTextContent(
"16,264.3 / 20,000",
);
await expect(getMetricValue(canvas, "Concurrent chats")).toHaveTextContent(
"Unlimited",
);
const manageUsage = canvas.getByRole("link", { name: "Manage usage" });
await expect(manageUsage).toHaveAttribute("href", "/deployment/groups");
const agentSettings = canvas.getByRole("link", { name: "Agent settings" });
await expect(agentSettings).toHaveAttribute(
"href",
"/ai/settings/coder-agents",
);
},
};

export const TooltipInteractions: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("open the Total Agent hours tooltip from keyboard", async () => {
await userEvent.tab();
await expect(
canvas.getByRole("button", { name: "Total Agent hours information" }),
).toHaveFocus();
await waitFor(async () => {
await expect(screen.getByRole("tooltip")).toHaveTextContent(
"Total agent runtime hours used out of the hours included in this license.",
);
});
await userEvent.keyboard("{Escape}");
await waitFor(async () => {
await expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
});
});
await step("open the Concurrent chats tooltip on hover", async () => {
await userEvent.hover(
canvas.getByRole("button", { name: "Concurrent chats information" }),
);
await waitFor(async () => {
await expect(screen.getByRole("tooltip")).toHaveTextContent(
"Number of Coder Agents chats that can run at the same time.",
);
});
});
},
};

export const UnlimitedAllocation: Story = {
args: {
allocation: -1,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(getMetricValue(canvas, "Total Agent hours")).toHaveTextContent(
"Unlimited",
);
await expect(getMetricValue(canvas, "Concurrent chats")).toHaveTextContent(
"Unlimited",
);
},
};

export const NotProvidingUsage: Story = {
args: {
actual: undefined,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(getMetricValue(canvas, "Total Agent hours")).toHaveTextContent(
"\u2014 / 20,000",
);
},
};

export const SoftLimitReached: Story = {
args: {
actual: 16264.3,
isSoftLimitReached: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(getMetricValue(canvas, "Total Agent hours")).toHaveTextContent(
"16,264.3 / 20,000",
);
await expect(canvas.getByRole("status")).toHaveTextContent(
"Approaching hours limit",
);
await expect(getMetricValue(canvas, "Concurrent chats")).toHaveTextContent(
"Unlimited",
);
},
};

export const Exceeded: Story = {
args: {
actual: 21000,
isExceeded: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(getMetricValue(canvas, "Total Agent hours")).toHaveTextContent(
"21,000.0 / 20,000",
);
await expect(getMetricValue(canvas, "Concurrent chats")).toHaveTextContent(
"Unlimited",
);
await expect(canvas.queryByRole("status")).not.toBeInTheDocument();
},
};

export const HardLimitExceeded: Story = {
args: {
actual: 25000,
isHardLimitExceeded: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(getMetricValue(canvas, "Total Agent hours")).toHaveTextContent(
"25,000.0 / 20,000",
);
await expect(getMetricValue(canvas, "Concurrent chats")).toHaveTextContent(
"5",
);
await expect(canvas.getByRole("status")).toHaveTextContent("Limit reached");
await userEvent.hover(
canvas.getByRole("button", { name: "Concurrent chats information" }),
);
await waitFor(async () => {
await expect(screen.getByRole("tooltip")).toHaveTextContent(
"Number of Coder Agents chats that can run at the same time. You've reached your limit: concurrent chats are now capped at 5 (down from unlimited).",
);
});
},
};

export const NoAllocation: Story = {
args: {
allocation: undefined,
actual: undefined,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
getMetricValue(canvas, "Max concurrent chats"),
).toHaveTextContent("5");
await expect(
canvas.queryByText(/Agent hours used/),
).not.toBeInTheDocument();
const upgrade = canvas.getByRole("link", { name: "Upgrade" });
await expect(upgrade).toHaveAttribute("href", "mailto:[email protected]");
},
};

export const NoAllocationWithUsage: Story = {
args: {
allocation: undefined,
actual: 1234.5,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(getMetricValue(canvas, "Agent hours used")).toHaveTextContent(
"1,234.5",
);
await expect(
canvas.getByRole("link", { name: "Upgrade" }),
).toBeInTheDocument();
},
};
Loading
Loading