feat: signed releases, release feed, notification (read-only)

This commit is contained in:
fsylvain committed 2026-10-01 15:01:53 +02:00
1 parent a49eec909c
commit 834493a4f5
49 files changed
+4191 -18

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
<script setup lang="ts">
import { computed } from 'vue'
import ReleaseNotes from '@/components/ReleaseNotes.vue'
import ServerCommand from '@/components/ServerCommand.vue'
import UpdateStatusBlock, { type UpdateTone } from '@/components/UpdateStatusBlock.vue'
import type { UpdateStatus } from '@/composables/useUpdate'
import type { UserRole } from '@/composables/useApi'
import type { UpdateCheckPhase } from '@/stores/update'
import {
ENABLE_BROWSER_UPDATES_COMMAND,
displayVersion,
formatLastChecked,
formatReleased,
} from '@/lib/updateFormat'
interface Block {
tone: UpdateTone
icon: string
title: string
text: string
meta?: string
}
interface ActionButton {
label: string
icon: string
primary: boolean
disabled: boolean
}
const props = defineProps<{
status: UpdateStatus
role: UserRole | undefined
phase: UpdateCheckPhase
retryLabel: string
retryActive: boolean
}>()
const emit = defineEmits<{
check: []
}>()
const isOwner = computed(() => props.role === 'owner')
const newVersion = computed(() => displayVersion(props.status.latest))
const lastChecked = computed(() =>
props.status.checked_at ? formatLastChecked(props.status.checked_at) : undefined,
)
const block = computed<Block>(() => {
if (isOwner.value && props.phase === 'checking') {
return {
tone: 'neutral',
icon: 'i-lucide-loader-circle',
title: 'Checking for updates…',
text: 'This usually takes a few seconds.',
}
}
if (isOwner.value && props.phase === 'limited') {
return {
tone: 'info',
icon: 'i-lucide-info',
title: 'Too many tries',
text: 'Wait a moment before trying again.',
}
}
if (isOwner.value && props.phase === 'failed') {
return {
tone: 'warning',
icon: 'i-lucide-wifi-off',
title: "Couldn't check for updates",
text: "RenewCMS couldn't reach the update server. Check your connection and try again. Your site isn't affected.",
meta: lastChecked.value,
}
}
if (props.status.available && isOwner.value) {
return {
tone: 'info',
icon: 'i-lucide-info',
title: `Version ${newVersion.value} is available`,
text: "This site can't update itself from the browser yet. Whoever manages your server can set it up once, or update RenewCMS for you.",
}
}
if (props.status.available) {
return {
tone: 'info',
icon: 'i-lucide-circle-arrow-up',
title: `Version ${newVersion.value} is available`,
text: `You have ${props.status.current}. Only the owner can update RenewCMS. Ask the owner of this site.`,
}
}
return {
tone: 'success',
icon: 'i-lucide-circle-check',
title: "You're up to date",
text: isOwner.value
? 'You have the latest version of RenewCMS.'
: 'Only the owner can update or restart the site.',
meta: isOwner.value ? lastChecked.value : undefined,
}
})
const showServerGuidance = computed(
() => isOwner.value && props.status.available && props.phase === 'idle',
)
const actionButton = computed<ActionButton | null>(() => {
if (!isOwner.value) {
return null
}
if (props.phase === 'checking') {
return {
label: 'Checking…',
icon: 'i-lucide-loader-circle',
primary: false,
disabled: true,
}
}
if (props.phase === 'failed') {
return { label: 'Try again', icon: 'i-lucide-rotate-cw', primary: true, disabled: false }
}
if (props.phase === 'limited') {
return props.retryActive
? {
label: `Try again in ${props.retryLabel}`,
icon: 'i-lucide-clock',
primary: false,
disabled: true,
}
: { label: 'Try again', icon: 'i-lucide-rotate-cw', primary: false, disabled: false }
}
if (props.status.available) {
return null
}
return {
label: 'Check for updates',
icon: 'i-lucide-refresh-cw',
primary: false,
disabled: false,
}
})
const notes = computed(() => {
const items = props.status.notes?.items ?? []
if (!showServerGuidance.value || items.length === 0 || !props.status.notes) {
return null
}
return {
items,
date: formatReleased(props.status.notes.released_at),
}
})
</script>
<template>
<section
aria-labelledby="updates-h"
data-testid="updates-card"
class="flex flex-col gap-4 border border-default bg-default p-4 lg:p-5"
>
<h2
id="updates-h"
tabindex="-1"
class="self-start text-lg font-semibold text-highlighted focus:outline-2 focus:outline-offset-4 focus:outline-primary"
>
Updates
</h2>
<UpdateStatusBlock
:tone="block.tone"
:icon="block.icon"
:title="block.title"
:text="block.text"
:meta="block.meta"
/>
<ReleaseNotes v-if="notes" :version="newVersion" :date="notes.date" :items="notes.items" />
<ServerCommand v-if="showServerGuidance" :line="ENABLE_BROWSER_UPDATES_COMMAND" />
<div v-if="actionButton" class="flex flex-col gap-2 md:flex-row md:items-center">
<UButton
:color="actionButton.primary ? 'primary' : 'neutral'"
:variant="actionButton.primary ? 'solid' : 'outline'"
:icon="actionButton.icon"
:disabled="actionButton.disabled"
class="h-11 justify-center gap-1.5 px-3.5 md:h-8"
:class="
actionButton.primary
? ''
: 'border border-accented bg-default text-highlighted ring-0'
"
:ui="{ leadingIcon: 'size-4' }"
@click="emit('check')"
>
{{ actionButton.label }}
</UButton>
</div>
<a
v-if="showServerGuidance && status.release_url"
:href="status.release_url"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 self-start text-sm font-medium text-highlighted underline underline-offset-3"
>
How to update
<UIcon name="i-lucide-arrow-up-right" class="size-3.5" />
</a>
</section>
</template>