feat: ajouter des composants de statut en direct et mettre à jour la configuration de l'éducation

This commit is contained in:
2025-12-24 19:34:29 +01:00
parent fb22fdf057
commit 91422148dd
17 changed files with 409 additions and 316 deletions

View File

@@ -1,5 +1,12 @@
NUXT_PUBLIC_I18N_BASE_URL=
NUXT_API_URL=
STUDIO_GITHUB_CLIENT_ID=
STUDIO_GITHUB_CLIENT_SECRET=
NUXT_DISCORD_USER_ID=
NUXT_WAKATIME_CODING=
NUXT_WAKATIME_EDITORS=
NUXT_WAKATIME_LANGUAGES=
NUXT_WAKATIME_OS=
NUXT_WAKATIME_USER_ID=
NUXT_STATUS_PAGE=

View File

@@ -1,125 +0,0 @@
<script lang="ts" setup>
import type { Activity } from '~~/types'
import { IDEs } from '~~/types'
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'), { lazy: true })
useIntervalFn(refresh, 5000)
const currentSession = computed(() => {
const list = activity.value?.data.activities ?? []
const ideActivity = list.find(a => IDEs.some(ide => ide.name === a.name))
if (!ideActivity) return null
const name = ideActivity.assets?.small_text === 'Cursor' ? 'Cursor' : ideActivity.name
const isIdling = ideActivity.details?.includes('Idling') || (!ideActivity.state?.toLowerCase().includes('editing') && name !== 'Visual Studio Code')
const rawProject = ideActivity.details ? ideActivity.details.replace('Workspace:', '').replace('Editing', '').trim() : 'Unknown Context'
const project = rawProject.charAt(0).toUpperCase() + rawProject.slice(1)
const file = ideActivity.state?.replace('Editing', '').trim() || 'No active file'
return {
name,
project,
file,
isIdling,
startTime: ideActivity.timestamps?.start,
icon: IDEs.find(ide => ide.name === name)?.icon ?? 'i-ph-code-duotone'
}
})
const timeAgo = useTimeAgo(computed(() => currentSession.value?.startTime ?? new Date()))
const statusColor = computed(() => {
if (!currentSession.value) return 'red'
return currentSession.value.isIdling ? 'orange' : 'green'
})
const statusLabel = computed(() => {
if (!currentSession.value) return 'System Offline'
if (currentSession.value.isIdling) return 'System Idling'
return 'Active Development'
})
</script>
<template>
<ClientOnly>
<div class="w-full mb-4">
<UCard v-if="activity">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<div class="relative flex h-3 w-3">
<span
v-if="statusColor === 'green'"
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-green-400"
/>
<span
class="relative inline-flex rounded-full h-3 w-3 transition-colors duration-300"
:class="{
'bg-green-500': statusColor === 'green',
'bg-orange-500': statusColor === 'orange',
'bg-red-500': statusColor === 'red'
}"
/>
</div>
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
{{ statusLabel }}
</span>
</div>
<UIcon
v-if="currentSession"
:name="currentSession.icon"
class="w-5 h-5 opacity-80"
/>
<UIcon
v-else
name="i-ph-power-duotone"
class="w-5 h-5 text-red-400 opacity-80"
/>
</div>
<div
v-if="currentSession"
class="space-y-1 pl-6 border-l-2 border-neutral-100 dark:border-neutral-800 ml-1.5"
>
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<h3 class="font-semibold text-neutral-900 dark:text-white truncate">
{{ currentSession.project }}
</h3>
<span class="hidden sm:inline text-neutral-400 text-xs"></span>
<span class="text-sm text-neutral-500 dark:text-neutral-400 truncate">
{{ currentSession.file }}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-neutral-400 mt-1">
<UIcon
name="i-ph-timer-duotone"
class="w-4 h-4"
/>
<span>Started {{ timeAgo }}</span>
</div>
</div>
<div
v-else
class="text-sm text-neutral-500 dark:text-neutral-400 flex items-center gap-2 pl-6 border-l-2 border-red-100 dark:border-red-900/30 ml-1.5"
>
<p>Telemetry disconnected. Research in progress.</p>
</div>
</UCard>
<UCard v-else>
<div class="flex items-center gap-3">
<USkeleton class="h-3 w-3 rounded-full" /> <div class="space-y-2 flex-1">
<USkeleton class="h-4 w-1/3" />
<USkeleton class="h-3 w-2/3" />
</div>
</div>
</UCard>
</div>
</ClientOnly>
</template>

View File

@@ -1,61 +0,0 @@
<script lang="ts" setup>
const { education } = await useContent()
const formatDate = (iso?: string) => {
if (!iso) return 'Present'
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return iso
return useDateFormat(d, 'MMM YYYY', { locales: 'en-US' }).value
}
</script>
<template>
<section
v-if="education && education.length"
class="my-8 space-y-6"
aria-labelledby="education-title"
>
<h2
id="education-title"
class="sr-only"
>
Education
</h2>
<div class="grid gap-4 grid-cols-1">
<UCard
v-for="item in education"
:key="item.id"
variant="outline"
color="neutral"
>
<div>
<h3 class="text-lg md:text-xl font-semibold tracking-tight">
{{ item.degree ?? item.title }}
</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-2">
{{ item.institution }}
</p>
<p class="text-sm text-neutral-700 dark:text-neutral-300 flex flex-wrap items-center gap-2">
<span class="font-medium">Dates:</span>
<span>{{ formatDate(item.startDate) }} {{ formatDate(item.endDate) }}</span>
<span
v-if="item.duration"
class="text-neutral-500"
>({{ item.duration }})</span>
</p>
<p
class="text-sm text-neutral-700 dark:text-neutral-300 mt-1"
>
<span class="font-medium">Location:</span> {{ item.location }}
</p>
<p
class="text-sm text-neutral-700 dark:text-neutral-300 mt-3"
>
{{ item.description }}
</p>
</div>
</UCard>
</div>
</section>
</template>

View File

@@ -1,100 +0,0 @@
<script lang="ts" setup>
const { experiences } = await useContent()
const formatDate = (iso?: string) => {
if (!iso) return 'Present'
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return iso
return useDateFormat(d, 'MMM YYYY', { locales: 'en-US' }).value
}
</script>
<template>
<section
v-if="experiences && experiences.length"
class="my-8 space-y-6"
aria-labelledby="experiences-title"
>
<h2
id="experiences-title"
class="sr-only"
>
Experiences
</h2>
<div class="grid gap-4 grid-cols-1">
<UCard
v-for="item in experiences"
:key="item.id"
variant="outline"
color="neutral"
>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div
v-if="item.emoji"
class="text-2xl leading-none"
>
{{ item.emoji }}
</div>
<div class="flex-1">
<h3 class="text-lg md:text-xl font-semibold tracking-tight">
{{ item.title }}<span
v-if="item.type"
class="text-md text-neutral-500 font-normal"
> · {{ item.type }}</span>
</h3>
<p class="text-sm text-neutral-700 dark:text-neutral-300">
<span class="font-medium mr-2">Company:</span>
<span
v-if="item.companyUrl"
class="underline decoration-neutral-400/70 underline-offset-4 hover:decoration-neutral-600 dark:hover:decoration-neutral-300"
>
<NuxtLink
:to="item.companyUrl"
target="_blank"
rel="noreferrer"
>{{ item.company }}</NuxtLink>
</span>
<span v-else>{{ item.company }}</span>
</p>
</div>
</div>
<p class="text-sm text-neutral-700 dark:text-neutral-300 flex flex-wrap items-center gap-2">
<span class="font-medium">Dates:</span>
<span>{{ formatDate(item.startDate) }} {{ formatDate(item.endDate) }}</span>
<span
v-if="item.duration"
class="text-neutral-500"
>({{ item.duration }})</span>
</p>
<p class="text-sm text-neutral-700 dark:text-neutral-300 flex items-center gap-2">
<span class="font-medium">Location:</span>
<span>{{ item.location }}</span>
</p>
<p class="text-sm text-neutral-700 dark:text-neutral-300">
{{ item.description }}
</p>
<div
v-if="item.tags?.length"
class="flex flex-wrap gap-2"
>
<UBadge
v-for="tag in item.tags"
:key="tag"
size="sm"
variant="soft"
color="primary"
>
{{ tag }}
</UBadge>
</div>
</div>
</UCard>
</div>
</section>
</template>

View File

@@ -0,0 +1,123 @@
<script lang="ts" setup>
import type { Activity } from '~~/types'
import { IDEs } from '~~/types'
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'), { lazy: true })
useIntervalFn(refresh, 5000)
const currentSession = computed(() => {
const list = activity.value?.data.activities ?? []
const ideActivity = list.find(a => IDEs.some(ide => ide.name === a.name))
if (!ideActivity) return null
const name = ideActivity.assets?.small_text === 'Cursor' ? 'Cursor' : ideActivity.name
const isIdling = ideActivity.details?.includes('Idling') || (!ideActivity.state?.toLowerCase().includes('editing') && name !== 'Visual Studio Code')
const rawProject = ideActivity.details ? ideActivity.details.replace('Workspace:', '').replace('Editing', '').trim() : 'Unknown Context'
const project = rawProject.charAt(0).toUpperCase() + rawProject.slice(1)
const file = ideActivity.state?.replace('Editing', '').trim() || 'No active file'
return {
name,
project,
file,
isIdling,
startTime: ideActivity.timestamps?.start,
icon: IDEs.find(ide => ide.name === name)?.icon ?? 'i-ph-code-duotone'
}
})
const timeAgo = useTimeAgo(computed(() => currentSession.value?.startTime ?? new Date()))
const statusColor = computed(() => {
if (!currentSession.value) return 'red'
return currentSession.value.isIdling ? 'orange' : 'green'
})
const statusLabel = computed(() => {
if (!currentSession.value) return 'System Offline'
if (currentSession.value.isIdling) return 'System Idling'
return 'Active Development'
})
</script>
<template>
<ClientOnly>
<UCard v-if="activity">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<div class="relative flex h-3 w-3">
<span
v-if="statusColor === 'green'"
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-green-400"
/>
<span
class="relative inline-flex rounded-full h-3 w-3 transition-colors duration-300"
:class="{
'bg-green-500': statusColor === 'green',
'bg-orange-500': statusColor === 'orange',
'bg-red-500': statusColor === 'red'
}"
/>
</div>
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
{{ statusLabel }}
</span>
</div>
<UIcon
v-if="currentSession"
:name="currentSession.icon"
class="w-5 h-5 opacity-80"
/>
<UIcon
v-else
name="i-ph-power-duotone"
class="w-5 h-5 text-red-400 opacity-80"
/>
</div>
<div
v-if="currentSession"
class="space-y-1 pl-6 border-l-2 border-neutral-100 dark:border-neutral-800 ml-1.5"
>
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<h3 class="font-semibold text-neutral-900 dark:text-white truncate">
{{ currentSession.project }}
</h3>
<span class="hidden sm:inline text-neutral-400 text-xs"></span>
<span class="text-sm text-neutral-500 dark:text-neutral-400 truncate">
{{ currentSession.file }}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-neutral-400 mt-1">
<UIcon
name="i-ph-timer-duotone"
class="w-4 h-4"
/>
<span>Started {{ timeAgo }}</span>
</div>
</div>
<div
v-else
class="text-sm text-neutral-500 dark:text-neutral-400 flex items-center gap-2 pl-6 border-l-2 border-red-100 dark:border-red-900/30 ml-1.5"
>
<p>Telemetry disconnected. Research in progress.</p>
</div>
</UCard>
<UCard v-else>
<div class="flex items-center gap-3">
<USkeleton class="h-3 w-3 rounded-full" /> <div class="space-y-2 flex-1">
<USkeleton class="h-4 w-1/3" />
<USkeleton class="h-3 w-2/3" />
</div>
</div>
</UCard>
</ClientOnly>
</template>

View File

@@ -0,0 +1,192 @@
<script lang="ts" setup>
import type { StatusPageData } from '~~/types'
const { data, status } = await useAsyncData<StatusPageData>('home-status', () =>
$fetch('/api/status-page'),
{ lazy: true }
)
const isLoading = computed(() => status.value === 'pending')
const metrics = computed(() => {
if (!data.value || !data.value.publicGroupList) {
return { up: 0, down: 0, maintenance: 0, total: 0, uptime: 0 }
}
let upCount = 0
let downCount = 0
let totalCount = 0
data.value.publicGroupList.forEach((group) => {
group.monitorList.forEach((monitor) => {
totalCount++
const isUp = (monitor as unknown as { status: number }).status !== 0
if (isUp) upCount++
else downCount++
})
})
const activeMaintenances = data.value.maintenanceList?.filter(m => m.active).length || 0
const uptimePercent = totalCount > 0 ? ((upCount / totalCount) * 100).toFixed(1) : '0.0'
return {
up: upCount,
down: downCount,
maintenance: activeMaintenances,
total: totalCount,
uptime: Number(uptimePercent)
}
})
const statusState = computed(() => {
if (isLoading.value) return { color: 'neutral' as const, label: 'Checking status...' }
if (metrics.value.down > 0) return { color: 'red' as const, label: 'Service Disruption' }
if (metrics.value.maintenance > 0) return { color: 'blue' as const, label: 'Maintenance Mode' }
return { color: 'emerald' as const, label: 'All Systems Operational' }
})
</script>
<template>
<ClientOnly>
<UCard class="h-full flex flex-col overflow-hidden">
<div class="p-5 border-b border-neutral-200 dark:border-neutral-800">
<div class="flex items-center justify-between mb-2">
<h3 class="font-bold text-neutral-900 dark:text-white text-sm">
System Status
</h3>
<div class="flex items-center gap-2">
<span
v-if="!isLoading"
class="relative flex h-2.5 w-2.5"
>
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
:class="`bg-${statusState.color}-400`"
/>
<span
class="relative inline-flex rounded-full h-2.5 w-2.5"
:class="`bg-${statusState.color}-500`"
/>
</span>
<USkeleton
v-else
class="h-2.5 w-2.5 rounded-full"
/>
<span
v-if="!isLoading"
class="text-xs font-mono font-medium"
:class="`text-${statusState.color}-600 dark:text-${statusState.color}-400`"
>
{{ statusState.label }}
</span>
<USkeleton
v-else
class="h-4 w-24"
/>
</div>
</div>
<div class="mt-4">
<div class="flex justify-between text-xs mb-1.5">
<span class="text-neutral-500">Global Uptime</span>
<span
v-if="!isLoading"
class="font-mono font-bold text-neutral-900 dark:text-white"
>{{ metrics.uptime }}%</span>
<USkeleton
v-else
class="h-4 w-8"
/>
</div>
<UProgress
v-if="!isLoading"
v-model="metrics.uptime"
:color="statusState.color"
size="sm"
/>
<USkeleton
v-else
class="h-2 w-full rounded-full"
/>
</div>
</div>
<div class="grid grid-cols-3 divide-x divide-neutral-200 dark:divide-neutral-800 flex-1">
<div class="p-4 flex flex-col items-center justify-center text-center group">
<span class="text-[10px] text-neutral-400 font-bold uppercase tracking-wider mb-1">Operational</span>
<div
v-if="!isLoading"
class="flex items-center gap-1.5 text-emerald-500"
>
<UIcon
name="i-ph-check-circle-duotone"
class="w-5 h-5"
/>
<span class="text-xl font-bold text-neutral-900 dark:text-white">{{ metrics.up }}</span>
</div>
<USkeleton
v-else
class="h-6 w-8 mt-1"
/>
</div>
<div class="p-4 flex flex-col items-center justify-center text-center">
<span class="text-[10px] text-neutral-400 font-bold uppercase tracking-wider mb-1">Down</span>
<div
v-if="!isLoading"
class="flex items-center gap-1.5"
:class="metrics.down > 0 ? 'text-red-500' : 'text-neutral-400'"
>
<UIcon
name="i-ph-warning-circle-duotone"
class="w-5 h-5"
/>
<span class="text-xl font-bold text-neutral-900 dark:text-white">{{ metrics.down }}</span>
</div>
<USkeleton
v-else
class="h-6 w-8 mt-1"
/>
</div>
<div class="p-4 flex flex-col items-center justify-center text-center">
<span class="text-[10px] text-neutral-400 font-bold uppercase tracking-wider mb-1">Maint.</span>
<div
v-if="!isLoading"
class="flex items-center gap-1.5"
:class="metrics.maintenance > 0 ? 'text-amber-500' : 'text-neutral-400'"
>
<UIcon
name="i-ph-wrench-duotone"
class="w-5 h-5"
/>
<span class="text-xl font-bold text-neutral-900 dark:text-white">{{ metrics.maintenance }}</span>
</div>
<USkeleton
v-else
class="h-6 w-8 mt-1"
/>
</div>
</div>
<div class="p-2 text-center border-t border-neutral-200 dark:border-neutral-800 mt-auto">
<UButton
to="https://go.arthurdanjou.fr/status"
target="_blank"
rel="noopener noreferrer"
variant="link"
color="neutral"
size="xs"
:padded="false"
class="text-xs hover:text-primary-500"
>
View detailed report
</UButton>
</div>
</UCard>
</ClientOnly>
</template>

View File

@@ -30,7 +30,7 @@ const items = computed<TimelineItem[]>(() => {
title: item.title || 'Degree',
description: item.institution || '',
date: formatDate(item.startDate, item.endDate, item.duration),
icon: item.icon || 'i-ph-graduation-cap-duotone' // Context-aware default icon
icon: item.icon || 'i-ph-graduation-cap-duotone'
}))
})
</script>
@@ -41,7 +41,7 @@ const items = computed<TimelineItem[]>(() => {
<UTimeline
:orientation="orientation"
:items="items"
:default-value="items.length"
:default-value="items.length - 1"
active-color="primary"
color="neutral"
class="w-full max-w-5xl"

View File

@@ -62,7 +62,6 @@ export const collections = {
source: 'education/*.md',
schema: z.object({
title: z.string(),
type: z.string().optional(),
degree: z.string().optional(),
institution: z.string(),
startDate: z.string(),

View File

@@ -1,6 +1,6 @@
---
title: Bachelor's Degree in Mathematics
type: Bachelor
degree: Bachelor
institution: Paris-Saclay University
location: Paris, France
startDate: 2021-09

View File

@@ -0,0 +1,16 @@
---
title: PhD Candidate: AI Safety & Mathematical Robustness
degree: Doctorate
institution: Academic Labs
location: Paris / International
startDate: 2026-10
endDate: undefined
duration: 3 years
description: I am actively seeking a PhD position starting in Fall 2026. My research interest lies at the intersection of Applied Mathematics and Deep Learning, specifically focusing on AI Safety, Adversarial Robustness, and Formal Verification. I aim to contribute to developing mathematically grounded methods to ensure the reliability and alignment of modern AI systems.
tags:
- AI Safety
- Robustness
- Formal Verification
- Applied Mathematics
icon: i-ph-student-duotone
---

View File

@@ -1,6 +1,6 @@
---
title: Master's Degree in Applied Mathematics (Year 1)
type: Master
degree: Master
institution: Paris Dauphine-PSL University
location: Paris, France
startDate: 2024-09

View File

@@ -1,6 +1,6 @@
---
title: Master's Degree in Applied Mathematics (Year 2)
type: Master
degree: Master
institution: Paris Dauphine-PSL University
location: Paris, France
startDate: 2025-09

View File

@@ -51,12 +51,14 @@ Mathematical rigor is the cornerstone of Safe AI. My background in :hover-text{t
Research requires discipline and transparency. Here is a real-time overview of my :hover-text{text="current environment" hover="OS, Editor, Activity"} and historical data.
::home-activity
::home-live-status-page{class="mb-4"}
::
::home-stats
::home-live-activity{class="mb-4"}
::
::home-live-stats
---
::home-quote

View File

@@ -3,6 +3,7 @@ import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'education')
.where('extension', '=', 'md')
.order('startDate', 'DESC')
.all()
if (result.length === 0) {
@@ -10,14 +11,6 @@ export default defineCachedEventHandler(async (event) => {
}
return result
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
.map(edu => ({
degree: edu.degree,
institution: edu.institution,
startDate: edu.startDate,
endDate: edu.endDate,
location: edu.location
}))
}, {
maxAge: 60 * 60 * 24,
name: 'education'

View File

@@ -3,6 +3,7 @@ import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'experiences')
.where('extension', '=', 'md')
.order('startDate', 'DESC')
.all()
if (result.length === 0) {
@@ -10,16 +11,6 @@ export default defineCachedEventHandler(async (event) => {
}
return result
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
.map(exp => ({
title: exp.title,
company: exp.company,
companyUrl: exp.companyUrl,
startDate: exp.startDate,
endDate: exp.endDate,
location: exp.location,
description: exp.description
}))
},
{
maxAge: 60 * 60 * 24,

View File

@@ -35,6 +35,62 @@ export interface Activity {
}
}
export interface StatusTag {
id: number
monitor_id: number
tag_id: number
value: string
name: string
color: string
}
export interface StatusMonitor {
id: number
name: string
sendUrl: number
type: string
url?: string
tags: StatusTag[]
}
export interface StatusGroup {
id: number
name: string
weight: number
monitorList: StatusMonitor[]
}
export interface StatusMaintenance {
id: number
title: string
description: string
strategy: string
active: boolean
status: string // 'under-maintenance', etc.
// ... autres champs optionnels (dateRange, etc.)
}
export interface StatusConfig {
slug: string
title: string
description: string
icon: string
autoRefreshInterval: number
theme: string
published: boolean
showTags: boolean
customCSS: string
footerText: string
showPoweredBy: boolean
}
export interface StatusPageData {
config: StatusConfig
incident: unknown | null
publicGroupList: StatusGroup[]
maintenanceList: StatusMaintenance[]
}
export const IDEs = [
{ name: 'Visual Studio Code', icon: 'i-logos:visual-studio-code' },
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-logos:intellij-idea' },