Skip to content

Commit 73982a6

Browse files
committed
feat: add support and maintenance section to health settings page
- Introduced a new support and maintenance card displaying support provider and key information. - Implemented support key verification logic with detailed validation messages. - Enhanced French translations for improved clarity in the user interface. - Updated the health settings page layout to accommodate new features while maintaining responsiveness.
1 parent 1be2080 commit 73982a6

File tree

1 file changed

+198
-3
lines changed

1 file changed

+198
-3
lines changed

apps/web/src/pages/settings/health.vue

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,46 @@
3333
q-card-section.row.items-center.justify-between
3434
.row.items-center.no-wrap
3535
q-icon(name='mdi-chart-box-multiple-outline' size='20px' class='q-mr-sm' color='primary')
36-
.text-subtitle2 Tendances detaillees (5 minutes)
37-
.text-caption.text-grey-7 Vue multi-indicateurs
36+
.text-subtitle2 Tendances detaillées (5 minutes)
3837
q-separator
3938
q-card-section
4039
.row.q-col-gutter-md
4140
.col-12
42-
.text-caption.text-grey-7.q-mb-xs Ressources systeme (CPU / Heap / RSS)
41+
.text-caption.text-grey-7.q-mb-xs Ressources système (CPU / Heap / RSS)
4342
client-only
4443
VChart(
4544
:option='resourcesTrendChartOptions'
4645
autoresize
4746
style='height: 260px; width: 100%;'
4847
)
4948

49+
q-card.q-mb-md(flat bordered)
50+
q-card-section.row.items-center.justify-between
51+
.row.items-center.no-wrap
52+
q-icon(name='mdi-lifebuoy' size='22px' class='q-mr-sm' :color='supportStatusColor')
53+
.text-subtitle1 Support & maintenance
54+
q-chip(
55+
square
56+
:color='supportStatusColor'
57+
text-color='white'
58+
:label='supportStatusLabel'
59+
)
60+
q-separator
61+
q-card-section
62+
.row.q-col-gutter-md
63+
.col-12.col-md-6
64+
.text-caption.text-grey-7 Support
65+
.text-subtitle2.text-weight-medium {{ supportProvider }}
66+
.col-12.col-md-6
67+
.text-caption.text-grey-7 Clé de support
68+
.row.items-center.no-wrap
69+
.text-subtitle2.text-weight-medium {{ supportKey }}
70+
q-spinner.q-ml-sm(v-if='supportKeyVerificationStatus === "checking"' color='primary' size='18px')
71+
q-icon.q-ml-sm(v-else-if='hasMaintenanceContract' :name='supportKeyValidationIcon' :color='supportKeyValidationColor' size='18px')
72+
q-tooltip.text-body2(v-if='hasMaintenanceContract' anchor='top middle' self='bottom middle')
73+
| {{ supportKeyValidationMessage }}
74+
.text-caption.text-grey-7.q-mt-sm {{ supportStatusMessage }}
75+
5076
.row.items-center.q-col-gutter-sm.q-mb-md
5177
.col
5278
.text-subtitle1.text-weight-bold Vue détaillée des services
@@ -202,6 +228,7 @@
202228
import { computed, watch } from 'vue'
203229
import VChart from 'vue-echarts'
204230
import ReconnectingEventSource from 'reconnecting-eventsource'
231+
import * as Sentry from '@sentry/nuxt'
205232
import { use } from 'echarts/core'
206233
import { LineChart } from 'echarts/charts'
207234
import { CanvasRenderer } from 'echarts/renderers'
@@ -239,6 +266,162 @@ export default defineNuxtComponent({
239266
VChart,
240267
},
241268
setup() {
269+
const runtimeConfig = useRuntimeConfig()
270+
const sentryDsn = computed(() => `${runtimeConfig.public?.sentry?.dsn || ''}`.trim())
271+
const supportProviderFromDomain = (domain: string): string => {
272+
const normalizedDomain = domain.toLowerCase()
273+
if (normalizedDomain.endsWith('libertech.fr')) {
274+
return 'Libertech-FR'
275+
}
276+
return domain
277+
}
278+
const parseSentrySupport = (dsn: string): { key: string; domain: string; provider: string } => {
279+
if (!dsn) {
280+
return { key: '-', domain: '-', provider: '-' }
281+
}
282+
283+
try {
284+
const parsedUrl = new URL(dsn)
285+
const key = decodeURIComponent(parsedUrl.username || '').trim() || '-'
286+
const domain = parsedUrl.hostname?.trim() || '-'
287+
const provider = domain === '-' ? '-' : supportProviderFromDomain(domain)
288+
return { key, domain, provider }
289+
} catch {
290+
return { key: '-', domain: '-', provider: '-' }
291+
}
292+
}
293+
const sentrySupport = computed(() => parseSentrySupport(sentryDsn.value))
294+
const hasMaintenanceContract = computed(() => sentryDsn.value.length > 0)
295+
const supportKey = computed(() => sentrySupport.value.key)
296+
const supportProvider = computed(() => sentrySupport.value.provider)
297+
const supportKeyFormatIsValid = computed(() => /^[a-f0-9]{32}$/i.test(supportKey.value))
298+
const supportKeyVerificationStatus = ref<'idle' | 'checking' | 'valid' | 'invalid'>('idle')
299+
const supportKeyValidationDetail = ref('')
300+
const verifySupportKeyWithSentry = async (): Promise<void> => {
301+
try {
302+
if (!hasMaintenanceContract.value) {
303+
supportKeyVerificationStatus.value = 'idle'
304+
supportKeyValidationDetail.value = ''
305+
return
306+
}
307+
308+
if (!supportKeyFormatIsValid.value) {
309+
supportKeyVerificationStatus.value = 'invalid'
310+
supportKeyValidationDetail.value = 'Format de cle Sentry invalide.'
311+
return
312+
}
313+
314+
supportKeyVerificationStatus.value = 'checking'
315+
supportKeyValidationDetail.value = ''
316+
317+
const getClientFn = (Sentry as unknown as { getClient?: () => unknown }).getClient
318+
if (typeof getClientFn !== 'function') {
319+
supportKeyVerificationStatus.value = 'invalid'
320+
supportKeyValidationDetail.value = 'Client Sentry indisponible.'
321+
return
322+
}
323+
324+
const sentryClient = getClientFn() as { getDsn?: () => { publicKey?: string } | undefined } | null
325+
const dsnPublicKey = sentryClient?.getDsn?.()?.publicKey || ''
326+
if (dsnPublicKey.toLowerCase() !== supportKey.value.toLowerCase()) {
327+
supportKeyVerificationStatus.value = 'invalid'
328+
supportKeyValidationDetail.value = 'Cle differente de celle initialisee par le SDK Sentry.'
329+
return
330+
}
331+
332+
const parsedUrl = new URL(sentryDsn.value)
333+
const projectId = parsedUrl.pathname.replace(/\//g, '')
334+
if (!projectId) {
335+
supportKeyVerificationStatus.value = 'invalid'
336+
supportKeyValidationDetail.value = 'ProjectId absent du DSN.'
337+
return
338+
}
339+
340+
const envelopeUrl = `${parsedUrl.origin}/api/${projectId}/envelope/?sentry_version=7&sentry_key=${encodeURIComponent(
341+
supportKey.value,
342+
)}&sentry_client=sesame.support-check`
343+
const envelopeBody = `${JSON.stringify({
344+
sent_at: new Date().toISOString(),
345+
dsn: sentryDsn.value,
346+
})}\n${JSON.stringify({ type: 'client_report' })}\n${JSON.stringify({
347+
timestamp: Math.floor(Date.now() / 1_000),
348+
discarded_events: [],
349+
})}`
350+
const response = await fetch(envelopeUrl, {
351+
method: 'POST',
352+
headers: {
353+
'Content-Type': 'text/plain;charset=UTF-8',
354+
},
355+
body: envelopeBody,
356+
})
357+
358+
if (response.ok) {
359+
supportKeyVerificationStatus.value = 'valid'
360+
supportKeyValidationDetail.value = ''
361+
return
362+
}
363+
364+
supportKeyVerificationStatus.value = 'invalid'
365+
let reason = ''
366+
try {
367+
const payload = (await response.json()) as { detail?: string }
368+
reason = payload?.detail || ''
369+
} catch {
370+
reason = ''
371+
}
372+
supportKeyValidationDetail.value = reason || `Validation Sentry en echec (${response.status} ${response.statusText}).`
373+
} catch {
374+
supportKeyVerificationStatus.value = 'invalid'
375+
supportKeyValidationDetail.value = 'Erreur reseau pendant la verification Sentry.'
376+
}
377+
}
378+
const supportKeyIsValid = computed(() => supportKeyVerificationStatus.value === 'valid')
379+
const supportKeyValidationIcon = computed(() => (supportKeyIsValid.value ? 'mdi-check-decagram' : 'mdi-alert-circle'))
380+
const supportKeyValidationColor = computed(() => (supportKeyIsValid.value ? 'positive' : 'warning'))
381+
const supportKeyValidationMessage = computed(() =>
382+
supportKeyVerificationStatus.value === 'checking'
383+
? 'Verification en cours via Sentry.'
384+
: supportKeyVerificationStatus.value === 'valid'
385+
? 'Cle de support valide (verification Sentry OK).'
386+
: supportKeyValidationDetail.value || 'Cle de support invalide.',
387+
)
388+
const supportStatusLabel = computed(() => {
389+
if (!hasMaintenanceContract.value) {
390+
return 'INACTIF'
391+
}
392+
if (supportKeyVerificationStatus.value === 'valid') {
393+
return 'ACTIF'
394+
}
395+
if (supportKeyVerificationStatus.value === 'checking' || supportKeyVerificationStatus.value === 'idle') {
396+
return 'VERIFICATION'
397+
}
398+
return 'INVALIDE'
399+
})
400+
const supportStatusColor = computed(() => {
401+
if (!hasMaintenanceContract.value) {
402+
return 'grey-7'
403+
}
404+
if (supportKeyVerificationStatus.value === 'valid') {
405+
return 'positive'
406+
}
407+
if (supportKeyVerificationStatus.value === 'checking' || supportKeyVerificationStatus.value === 'idle') {
408+
return 'primary'
409+
}
410+
return 'negative'
411+
})
412+
const supportStatusMessage = computed(() => {
413+
if (!hasMaintenanceContract.value) {
414+
return 'Maintenance inactive, support open source sur GitHub.'
415+
}
416+
if (supportKeyVerificationStatus.value === 'valid') {
417+
return 'Contrat de maintenance actif.'
418+
}
419+
if (supportKeyVerificationStatus.value === 'checking' || supportKeyVerificationStatus.value === 'idle') {
420+
return 'Verification de la clé de maintenance en cours...'
421+
}
422+
return 'Contrat de maintenance configuré, mais clé de maintenance invalide.'
423+
})
424+
242425
const { data, pending, error, refresh } = useHttp<HealthHttpResponse>('/core/health', {
243426
method: 'GET',
244427
})
@@ -769,6 +952,8 @@ export default defineNuxtComponent({
769952
}, 1_000)
770953
771954
onMounted(() => {
955+
verifySupportKeyWithSentry()
956+
772957
const sseUrl = new URL(window.location.origin + '/api/core/health/sse')
773958
const source = new ReconnectingEventSource(sseUrl.toString())
774959
healthSse.value = source
@@ -828,6 +1013,16 @@ export default defineNuxtComponent({
8281013
systemIcon,
8291014
futureCheckIcon,
8301015
formatMetricValue,
1016+
hasMaintenanceContract,
1017+
supportKey,
1018+
supportProvider,
1019+
supportKeyVerificationStatus,
1020+
supportKeyValidationIcon,
1021+
supportKeyValidationColor,
1022+
supportKeyValidationMessage,
1023+
supportStatusLabel,
1024+
supportStatusColor,
1025+
supportStatusMessage,
8311026
}
8321027
},
8331028
})

0 commit comments

Comments
 (0)