feat: show account storage usage
CI and Deploy / test (push) Successful in 2m37s
CI and Deploy / deploy (push) Successful in 58s

This commit is contained in:
cesnimda
2026-07-31 00:10:15 +02:00
parent 9f16a5675a
commit 10d548f799
3 changed files with 21 additions and 3 deletions
@@ -22,7 +22,7 @@ public sealed class AiUsageController : ControllerBase
}
public sealed record UsagePeriodDto(int Calls, long InputCharacters, long OutputCharacters, long EstimatedTokens);
public sealed record UsageDto(UsagePeriodDto CurrentMonth, UsagePeriodDto AllTime, string Plan, int MonthlyCallLimit, long MonthlyTokenLimit);
public sealed record UsageDto(UsagePeriodDto CurrentMonth, UsagePeriodDto AllTime, string Plan, int MonthlyCallLimit, long MonthlyTokenLimit, long StorageUsedBytes, long StorageLimitBytes);
[HttpGet]
public async Task<ActionResult<UsageDto>> Get(CancellationToken cancellationToken)
@@ -38,7 +38,9 @@ public sealed class AiUsageController : ControllerBase
await SumAsync(_db.AiInteractions.Where(x => x.OwnerUserId == user.Id), cancellationToken),
entitlements.AdvancedAi ? "premium" : "free",
entitlements.MonthlyAiCalls,
entitlements.MonthlyAiTokens));
entitlements.MonthlyAiTokens,
await _db.Attachments.Where(x => x.JobApplication.OwnerUserId == user.Id).SumAsync(x => (long?)x.FileSize, cancellationToken) ?? 0,
entitlements.StorageBytes));
}
private static async Task<UsagePeriodDto> SumAsync(IQueryable<AiInteraction> query, CancellationToken cancellationToken)
@@ -13,6 +13,8 @@ test("shows monthly AI call and token limits", async () => {
plan: "free",
monthlyCallLimit: 25,
monthlyTokenLimit: 100000,
storageUsedBytes: 50000000,
storageLimitBytes: 250000000,
},
});
@@ -20,5 +22,6 @@ test("shows monthly AI call and token limits", async () => {
expect(await screen.findByText("4 of 25 generations this month")).toBeInTheDocument();
expect(screen.getByText("12,000 of 100,000 estimated tokens")).toBeInTheDocument();
expect(screen.getByText("50.0 MB of 250.0 MB attachment storage")).toBeInTheDocument();
expect(screen.getByLabelText("Monthly AI generations used")).toHaveAttribute("aria-valuenow", "16");
});
+14 -1
View File
@@ -7,8 +7,16 @@ type Usage = {
plan: string;
monthlyCallLimit: number;
monthlyTokenLimit: number;
storageUsedBytes: number;
storageLimitBytes: number;
};
function formatBytes(bytes: number) {
if (bytes < 1_000_000) return `${Math.round(bytes / 1_000)} KB`;
if (bytes < 1_000_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
return `${(bytes / 1_000_000_000).toFixed(1)} GB`;
}
export default function AiUsageCard() {
const [usage, setUsage] = useState<Usage | null>(null);
const [failed, setFailed] = useState(false);
@@ -26,6 +34,7 @@ export default function AiUsageCard() {
const callsPercent = Math.min(100, usage.currentMonth.calls / usage.monthlyCallLimit * 100);
const tokensPercent = Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100);
const storagePercent = Math.min(100, usage.storageUsedBytes / usage.storageLimitBytes * 100);
return (
<Paper sx={{ p: 2.5, borderRadius: 4, border: "none" }}>
@@ -41,7 +50,11 @@ export default function AiUsageCard() {
<Typography variant="body2">{usage.currentMonth.estimatedTokens.toLocaleString()} of {usage.monthlyTokenLimit.toLocaleString()} estimated tokens</Typography>
<LinearProgress variant="determinate" value={tokensPercent} aria-label="Monthly AI tokens used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>Limits reset at the start of each calendar month.</Typography>
<Box sx={{ mt: 2 }}>
<Typography variant="body2">{formatBytes(usage.storageUsedBytes)} of {formatBytes(usage.storageLimitBytes)} attachment storage</Typography>
<LinearProgress variant="determinate" value={storagePercent} aria-label="Attachment storage used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>AI limits reset at the start of each calendar month.</Typography>
</Paper>
);
}