fix(ui): truthful uninstall progress bar (was a solid full-red block)

AppCard's uninstall bar was hardcoded `w-full bg-red-400/60 animate-pulse`
— a solid, full-width, red, fake-pulsing block that never moved and read
as an error, no matter the actual teardown progress (the install bar, by
contrast, renders a real percentage). Derive a truthful percentage from
the backend's existing `uninstall-stage` label — "Stopping containers
(X/N)" → 10–50%, "Cleaning up volumes" → 70%, "Removing app data" → 90%
— and render it exactly like install: neutral fill, real width + percent,
shimmer (not a fake pulse) carrying motion when a stage has no number.
Frontend-only; the backend already broadcasts these stages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-26 06:04:48 -04:00
parent 67426c0d41
commit 9f17ba6867

View File

@ -102,17 +102,23 @@
</div> </div>
</div> </div>
<!-- Uninstalling progress live stage label from backend --> <!-- Uninstalling progress truthful stage-driven bar (mirrors install) -->
<div v-else-if="isUninstalling" class="mt-4"> <div v-else-if="isUninstalling" class="mt-4">
<div class="flex items-center gap-1.5"> <div class="flex items-center justify-between mb-1.5">
<svg class="animate-spin h-3 w-3 text-red-400" fill="none" viewBox="0 0 24 24"> <span class="text-xs text-white/70 flex items-center gap-1.5">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
</svg> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<span class="text-xs text-red-300 truncate">{{ uninstallStageLabel }}</span> </svg>
{{ uninstallStageLabel }}
</span>
<span v-if="uninstallProgress !== null" class="text-xs text-white/50">{{ uninstallProgress }}%</span>
</div> </div>
<div class="mt-1.5 w-full h-1.5 bg-white/10 rounded-full overflow-hidden"> <div class="w-full h-1.5 bg-white/10 rounded-full overflow-hidden">
<div class="h-full bg-red-400/60 rounded-full animate-pulse w-full"></div> <div
class="install-progress-fill h-full bg-white/60 rounded-full transition-all duration-500"
:style="{ width: `${Math.max(uninstallProgress ?? 8, 4)}%` }"
></div>
</div> </div>
</div> </div>
@ -282,6 +288,29 @@ const uninstallStageLabel = computed(() => {
return raw ? raw : `${t('common.uninstalling')}` return raw ? raw : `${t('common.uninstalling')}`
}) })
// Map the backend's uninstall-stage label to a truthful percentage so the bar
// progresses through the teardown instead of sitting at a solid full(-red)
// block. Backend stages (set_uninstall_stage):
// "Stopping containers (X/N)" 1050% (linear over the stack)
// "Cleaning up volumes" 70%
// "Removing app data" 90%
// Unknown/between pushes null the bar parks low and the shimmer overlay
// (install-progress-fill) carries the motion, exactly like a fixed install phase.
const uninstallProgress = computed<number | null>(() => {
const raw = props.pkg['uninstall-stage'] || ''
const m = raw.match(/\((\d+)\s*\/\s*(\d+)\)/)
if (m) {
const done = Number(m[1])
const total = Number(m[2])
if (total > 0) {
return Math.round(10 + Math.min(done / total, 1) * 40)
}
}
if (/volume/i.test(raw)) return 70
if (/data/i.test(raw)) return 90
return null
})
const isTransitioning = computed(() => { const isTransitioning = computed(() => {
const s = props.pkg.state const s = props.pkg.state
const h = props.pkg.health const h = props.pkg.health