/** * useReleaseNotes Composable * * Fetches and displays versioned release notes from the DCS Portal API. * Supports fetching specific versions or the latest release. * * @example * ```vue * * * * Loading... * {{ error }} * * {{ releaseNote.title }} * {{ releaseNote.summary }} * * * * ``` */ import { platformFetch } from '@duffcloudservices/cms-core' import { ref, onMounted } from 'vue' import type { ReleaseNote, ReleaseNotesReturn } from '../types/release-notes' /** * Get environment variable value. */ function getEnvVar(key: string, defaultValue = ''): string { try { if (typeof import.meta !== 'undefined' && import.meta.env) { const value = (import.meta.env as Record)[key] if (value !== undefined) return value } } catch { // import.meta not available } return defaultValue } // Simple cache for release notes const releaseNotesCache = new Map() const CACHE_TTL = 5 * 60 * 1000 // 5 minutes /** * useReleaseNotes composable for fetching release notes from the DCS API. * * @param version - Semantic version (e.g., "1.2.0") or "latest" * @param options - Optional configuration * @returns Release notes data and state */ export function useReleaseNotes( version: string, options: { fetchOnMount?: boolean } = {} ): ReleaseNotesReturn { const { fetchOnMount = true } = options const apiBaseUrl = getEnvVar('VITE_API_BASE_URL', 'https://portal.duffcloudservices.com') // @deprecated VITE_SITE_SLUG: the site is now resolved server-side from the // request Host or the dedicated Container App's DCS_SITE_SLUG. It is retained // only for cache-key continuity and source compatibility, not for routing. const siteSlug = getEnvVar('VITE_SITE_SLUG', '') const releaseNote = ref(null) const isLoading = ref(false) const error = ref(null) async function fetchReleaseNotes(): Promise { // Check cache. The slug is no longer required for routing (server resolves // the site from Host / DCS_SITE_SLUG); it is only part of the cache key. const cacheKey = `${siteSlug}:${version}` const cached = releaseNotesCache.get(cacheKey) if (cached && cached.expiresAt > Date.now()) { releaseNote.value = cached.data return } isLoading.value = true error.value = null try { const url = `${apiBaseUrl}/api/v1/release-notes/${version}` // C-298 layer 2: an HTML body here means the call never reached the API (a host // whose Front Door config has no /api/v1/* route answers with the SPA shell at // HTTP 200). platformFetch names the URL + content-type instead of letting // `response.json()` throw an opaque "Unexpected token '<'" into the catch below. // A real 404 still lands in the `!response.ok` branch untouched. const response = await platformFetch(url, { headers: { Accept: 'application/json', }, }) if (!response.ok) { if (response.status === 404) { error.value = `Release notes for version ${version} not found` return } throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const data = await response.json() // Normalize the response const note: ReleaseNote = { version: data.version, title: data.title, summary: data.summary || '', notesMarkdown: data.notesMarkdown || data.notes || '', changeCount: data.changeCount || 0, releaseDate: data.releaseDate || data.releasedAt || '', } // Cache the result releaseNotesCache.set(cacheKey, { data: note, expiresAt: Date.now() + CACHE_TTL, }) releaseNote.value = note } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to load release notes' console.error('[@duffcloudservices/cms] Failed to fetch release notes:', e) } finally { isLoading.value = false } } async function refresh(): Promise { // Clear cache const cacheKey = `${siteSlug}:${version}` releaseNotesCache.delete(cacheKey) await fetchReleaseNotes() } // Fetch on mount if enabled if (fetchOnMount && typeof window !== 'undefined') { onMounted(() => { fetchReleaseNotes() }) } return { releaseNote, isLoading, error, refresh, } }
{{ releaseNote.summary }}