-
Notifications
You must be signed in to change notification settings - Fork 0
Add storage quota UI and upload guardrails #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds storage quota management features with a brutalist-styled UI. The implementation fetches and displays storage quota information across multiple components and prevents file uploads that would exceed the user's remaining storage.
Key changes:
- Added storage quota fetching and display on dashboard with real-time updates after operations
- Integrated remaining storage indicators in toolbar badge and user menu dropdown
- Implemented pre-upload validation to block files exceeding available storage with clear error messages
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ui/components/auth/UserMenu.ts | Added StorageQuota integration with formatBytes helper, updateQuota method, and quota display in dropdown with remaining storage pill |
| src/ui/components/Toolbar.ts | Added storage badge component with updateStorageBadge method and formatRemaining helper for displaying remaining quota |
| src/ui/components/Dashboard.ts | Added quota fetching on load, quota card rendering with skeleton states, error handling, and quota refresh after file deletion |
| src/ui/App.ts | Added fetchAndApplyQuota method to coordinate quota fetching and updates across components, integrated quota into authentication flow |
| src/styles.css | Added brutalist-styled CSS for storage badge, quota cards, progress bars, and skeleton loading states |
| src/lib/cloud/sync.ts | Added quota validation before file upload to prevent exceeding storage limits with detailed error messages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { used, quota } = this.storageQuota; | ||
| const percentUsed = Math.min( | ||
| 100, | ||
| Math.round(this.storageQuota.percentUsed ?? (used / quota) * 100) |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential division by zero when quota is 0. This would result in Infinity or NaN for the calculation, which could cause unexpected display issues.
Consider adding a check:
Math.round(this.storageQuota.percentUsed ?? (quota > 0 ? (used / quota) * 100 : 0))| Math.round(this.storageQuota.percentUsed ?? (used / quota) * 100) | |
| Math.round(this.storageQuota.percentUsed ?? (quota > 0 ? (used / quota) * 100 : 0)) |
| badge.textContent = `${remainingText} free`; | ||
| badge.setAttribute('aria-label', `${remainingText} remaining (${percent}% free)`); | ||
| badge.style.display = 'inline-flex'; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The storage badge lacks semantic information for screen readers when displayed. While there's an aria-label attribute, the badge itself doesn't have a role that indicates its purpose. Consider adding a role="status" attribute to make it clear this is a live status indicator:
badge.setAttribute('role', 'status');
badge.setAttribute('aria-label', `${remainingText} remaining (${percent}% free)`);| private formatRemaining(bytes: number): string { | ||
| const gb = bytes / (1024 * 1024 * 1024); | ||
| if (gb >= 0.1) { | ||
| return `${gb.toFixed(1)} GB`; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formatRemaining method can return negative values in the display string when mb is negative (e.g., if bytes is negative). The Math.max(mb, 0) only prevents negative MB values, but if bytes are negative, gb.toFixed(1) on line 277 could still return a negative value like "-0.5 GB".
Consider adding Math.max(0, gb) to ensure GB values are also non-negative:
if (gb >= 0.1) {
return `${Math.max(0, gb).toFixed(1)} GB`;
}| return `${gb.toFixed(1)} GB`; | |
| return `${Math.max(0, gb).toFixed(1)} GB`; |
| } | ||
|
|
||
| const remainingText = this.formatRemaining(remainingBytes); | ||
| const percent = Math.max(0, Math.round((remainingBytes / quotaBytes) * 100)); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential division by zero when quotaBytes is 0. This would result in Infinity or NaN for percent, which could cause confusing aria-label text like "0% free" when storage is actually full or "Infinity% free" when quota is zero.
Consider adding a check:
const percent = quotaBytes > 0 ? Math.max(0, Math.round((remainingBytes / quotaBytes) * 100)) : 0;| const percent = Math.max(0, Math.round((remainingBytes / quotaBytes) * 100)); | |
| const percent = quotaBytes > 0 ? Math.max(0, Math.round((remainingBytes / quotaBytes) * 100)) : 0; |
| Signed in | ||
| <div style="display: flex; align-items: center; gap: 8px;"> | ||
| <span style="font-size: 12px; color: #666;">Signed in</span> | ||
| <span class="quota-pill" style="font-size: 12px; padding: 4px 8px; border: 2px solid #111; border-radius: 999px; background: #fef3c7; color: #111; font-weight: 700; letter-spacing: -0.01em; box-shadow: 4px 4px 0 #111;"> |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Inline styles for the quota pill are duplicated between the UserMenu dropdown (lines 134-136) and potentially the CSS .quota-pill class. Consider using the CSS class consistently instead of mixing inline styles with the class attribute.
The current implementation applies both inline styles and a class name:
<span class="quota-pill" style="...">This can lead to maintainability issues and style conflicts. Either use the class with all styles in CSS, or remove the class if using inline styles.
| <span class="quota-pill" style="font-size: 12px; padding: 4px 8px; border: 2px solid #111; border-radius: 999px; background: #fef3c7; color: #111; font-weight: 700; letter-spacing: -0.01em; box-shadow: 4px 4px 0 #111;"> | |
| <span class="quota-pill"> |
| private formatBytes(bytes: number, short: boolean = false): string { | ||
| const gb = bytes / (1024 * 1024 * 1024); | ||
| const mb = bytes / (1024 * 1024); | ||
|
|
||
| if (gb >= 0.1) { | ||
| return short ? `${gb.toFixed(1)} GB` : `${gb.toFixed(2)}`; | ||
| } | ||
|
|
||
| return short ? `${mb.toFixed(0)} MB` : `${mb.toFixed(1)}`; | ||
| } |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This formatBytes helper is duplicated across multiple components with slightly different implementations (UserMenu, Dashboard). Consider extracting this into a shared utility module to maintain consistency and avoid duplication.
For example, create a src/lib/utils/format.ts file with:
export function formatBytes(bytes: number, options?: { short?: boolean }): string {
// Unified implementation
}| percentUsed: Math.round( | ||
| (this.user.storage_used_bytes / this.user.storage_quota_bytes) * 100 | ||
| ), |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential division by zero when this.user.storage_quota_bytes is 0. This would result in Infinity or NaN for percentUsed, which could cause unexpected display issues.
Consider adding a check:
percentUsed: this.user.storage_quota_bytes > 0
? Math.round((this.user.storage_used_bytes / this.user.storage_quota_bytes) * 100)
: 0,| percentUsed: Math.round( | |
| (this.user.storage_used_bytes / this.user.storage_quota_bytes) * 100 | |
| ), | |
| percentUsed: this.user.storage_quota_bytes > 0 | |
| ? Math.round((this.user.storage_used_bytes / this.user.storage_quota_bytes) * 100) | |
| : 0, |
Summary
Testing
Codex Task