feat(03-05): 3-step presigned upload + quota state in auth store + progress UI

- api/client.js: extend request() to attach .status and .payload on 413 structured errors; remove legacy uploadDocument multipart function
- stores/auth.js: add quota ref({used_bytes:0, limit_bytes:0}) and fetchQuota() action (silent catch); expose in store return
- stores/documents.js: replace single upload() with uploadToMinIO XHR helper + 3-step async action (getUploadUrl→XHR PUT→confirmUpload); track uploadProgress map keyed by filename+timestamp (T-03-25); call fetchQuota after upload success and document delete
- components/upload/UploadProgress.vue: add aria progressbar per row, percentage label, quota rejection error block (role=alert, red-50/red-200) from item.quotaError; use plain anchor for Manage storage link
This commit is contained in:
curo1305
2026-05-23 20:46:24 +02:00
parent 6bd57629ce
commit eb18428d07
4 changed files with 172 additions and 18 deletions
@@ -7,14 +7,64 @@
>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-800 truncate">{{ item.name }}</p>
<p v-if="item.error" class="text-xs text-red-500 mt-0.5">{{ item.error }}</p>
<p v-else-if="item.done" class="text-xs text-green-600 mt-0.5">
<!-- Error state (non-quota) -->
<p v-if="item.error" class="text-sm text-red-500 mt-0.5">{{ item.error }}</p>
<!-- Done state -->
<p v-else-if="item.done" class="text-sm text-green-600 mt-0.5">
Done{{ item.topics?.length ? ` classified as: ${item.topics.join(', ')}` : ' no topics assigned' }}
</p>
<p v-else class="text-xs text-gray-400 mt-0.5">Uploading</p>
<!-- Quota rejection error block (413 response T-03-23, UI-SPEC) -->
<div
v-else-if="item.quotaError"
role="alert"
class="mt-1 p-3 rounded-lg bg-red-50 border border-red-200"
>
<p class="text-sm font-semibold text-red-700">Not enough storage</p>
<p class="text-sm text-red-600 mt-1">
This file ({{ (item.quotaError.rejected_bytes / 1048576).toFixed(1) }} MB) would exceed your quota.
</p>
<p class="text-sm text-red-600">
You're using {{ (item.quotaError.used_bytes / 1048576).toFixed(1) }} MB of {{ (item.quotaError.limit_bytes / 1048576).toFixed(1) }} MB.
</p>
<!-- Plain anchor to avoid router-link import dependency in upload component -->
<a href="/settings" class="text-sm text-red-600 underline hover:text-red-700 font-semibold">
Manage storage →
</a>
</div>
<!-- In-progress: progress bar + percentage + status -->
<template v-else>
<!-- Progress bar track + fill (UI-SPEC Upload Progress Bar Contract) -->
<div
v-if="item.progress !== undefined"
class="w-full h-2 bg-gray-100 rounded-full mt-1 overflow-hidden"
>
<div
role="progressbar"
:aria-valuenow="item.progress"
aria-valuemin="0"
aria-valuemax="100"
:aria-label="`Upload progress for ${item.name}`"
class="h-full rounded-full transition-all duration-300 bg-indigo-500"
:style="{ width: `${item.progress}%` }"
></div>
</div>
<!-- Percentage label (shown during upload, hidden after completion) -->
<p
v-if="item.progress !== undefined"
class="text-sm text-gray-400 mt-1 text-right"
>{{ item.progress }}%</p>
<!-- Step status string (UI-SPEC Copywriting Contract) -->
<p class="text-sm text-gray-400 mt-0.5">{{ item.status || 'Uploading' }}</p>
</template>
</div>
<!-- Icon slot: error / done / spinner -->
<div class="shrink-0">
<svg v-if="item.error" class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<svg v-if="item.error || item.quotaError" class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<svg v-else-if="item.done" class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">